infra/hosts/www/caldav-export.py
2026-07-17 08:57:50 +02:00

120 lines
2.7 KiB
Python

import argparse
import os
import re
from pathlib import Path
import yaml
from caldav import DAVClient
from icalendar import Calendar
def safe_filename(name: str) -> str:
return re.sub(r"[^a-zA-Z0-9._-]+", "_", name)
def export_calendar(url: str, username: str, password: str, calendar: str) -> Calendar:
client = DAVClient(
url=url,
username=username,
password=password,
)
principal = client.principal()
calendars = principal.calendars()
target = None
for cal in calendars:
if cal.name == calendar:
target = cal
break
if target is None:
raise RuntimeError(
f"Calendar '{calendar}' not found for {username}"
)
print(f"Exporting {username}/{target.name}")
result = Calendar()
result.add("prodid", f"-//{calendar} calendar export//")
result.add("version", "2.0")
objects = target.objects()
print(f" {len(objects)} objects")
for obj in objects:
data = obj.data
if data is None:
print(f" fetching {obj.url}")
obj.load()
data = obj.data
if not data:
print(f" skipping empty object {obj.url}")
continue
cal = Calendar.from_ical(data)
for component in cal.walk():
if component.name != "VCALENDAR":
result.add_component(component)
return result
def main():
parser = argparse.ArgumentParser()
parser.add_argument("config")
parser.add_argument(
"-o",
"--output",
default="/srv/http/calendars",
)
args = parser.parse_args()
with open(args.config) as f:
config = yaml.safe_load(f)
output_dir = Path(args.output)
output_dir.mkdir(parents=True, exist_ok=True)
combined = Calendar()
combined.add("prodid", "-//Combined calendar//")
combined.add("version", "2.0")
username = config["username"]
password = config["password"]
for entry in config["calendars"]:
cal = export_calendar(
entry["url"],
username,
password,
entry["calendar"],
)
filename = output_dir / f"{safe_filename(entry['calendar'])}.ics"
with open(filename, "wb") as f:
f.write(cal.to_ical())
print(f" -> {filename}")
for component in cal.walk():
if component.name != "VCALENDAR":
combined.add_component(component)
combined_file = output_dir / "all.ics"
with open(combined_file, "wb") as f:
f.write(combined.to_ical())
print()
print(f"Combined export: {combined_file}")
if __name__ == "__main__":
main()