webdav to ical export
This commit is contained in:
parent
d545f5985f
commit
289e502d6a
6 changed files with 155 additions and 0 deletions
110
caldav-export.py
Normal file
110
caldav-export.py
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
from caldav import DAVClient
|
||||||
|
from icalendar import Calendar
|
||||||
|
|
||||||
|
|
||||||
|
def safe_filename(name):
|
||||||
|
return re.sub(r"[^a-zA-Z0-9._-]+", "_", name)
|
||||||
|
|
||||||
|
|
||||||
|
def export_calendar(url, username, password, output):
|
||||||
|
client = DAVClient(
|
||||||
|
url=url,
|
||||||
|
username=username,
|
||||||
|
password=password,
|
||||||
|
)
|
||||||
|
|
||||||
|
principal = client.principal()
|
||||||
|
|
||||||
|
calendars = principal.calendars()
|
||||||
|
|
||||||
|
target = None
|
||||||
|
for cal in calendars:
|
||||||
|
if cal.name == output["calendar"]:
|
||||||
|
target = cal
|
||||||
|
break
|
||||||
|
|
||||||
|
if target is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Calendar '{output['calendar']}' not found for {username}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Exporting {username}/{target.name}")
|
||||||
|
|
||||||
|
result = Calendar()
|
||||||
|
result.add("prodid", "-//CalDAV Export//")
|
||||||
|
result.add("version", "2.0")
|
||||||
|
|
||||||
|
objects = target.objects()
|
||||||
|
|
||||||
|
print(f" {len(objects)} objects")
|
||||||
|
|
||||||
|
for obj in objects:
|
||||||
|
cal = Calendar.from_ical(obj.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/ics",
|
||||||
|
)
|
||||||
|
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", "-//CalDAV Export Combined//")
|
||||||
|
combined.add("version", "2.0")
|
||||||
|
|
||||||
|
for entry in config["calendars"]:
|
||||||
|
username = entry["username"]
|
||||||
|
password = entry["password"]
|
||||||
|
|
||||||
|
cal = export_calendar(
|
||||||
|
entry["url"],
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
entry,
|
||||||
|
)
|
||||||
|
|
||||||
|
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-calendars.ics"
|
||||||
|
|
||||||
|
with open(combined_file, "wb") as f:
|
||||||
|
f.write(combined.to_ical())
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(f"Combined export: {combined_file}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
|
|
@ -110,6 +110,12 @@
|
||||||
group = "nginx";
|
group = "nginx";
|
||||||
mode = "0440";
|
mode = "0440";
|
||||||
};
|
};
|
||||||
|
caldav-export-config = {
|
||||||
|
file = ./secrets/caldav-export-config.age;
|
||||||
|
owner = "nobody";
|
||||||
|
group = "nobody";
|
||||||
|
mode = "0400";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
./hosts/www
|
./hosts/www
|
||||||
|
|
|
||||||
37
hosts/www/dav2ical.nix
Normal file
37
hosts/www/dav2ical.nix
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
{ config, pkgs, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
outputDir = "/srv/http/ical";
|
||||||
|
script = pkgs.writeScript "caldav-export.py" ''
|
||||||
|
#!${pkgs.python3}/bin/python3
|
||||||
|
|
||||||
|
${builtins.readFile ./caldav-export.py}
|
||||||
|
'';
|
||||||
|
pythonEnv = pkgs.python3.withPackages (ps: with ps; [
|
||||||
|
caldav
|
||||||
|
icalendar
|
||||||
|
pyyaml
|
||||||
|
]);
|
||||||
|
in
|
||||||
|
{
|
||||||
|
systemd = {
|
||||||
|
services.caldav-export = {
|
||||||
|
description = "Export CalDAV calendars to ICS";
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
ExecStart = "${pythonEnv}/bin/python ${script} ${config.age.secrets.caldav-export-config.path} -o ${outputDir}";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
timers.caldav-export = {
|
||||||
|
description = "Daily CalDAV export";
|
||||||
|
wantedBy = [
|
||||||
|
"timers.target"
|
||||||
|
];
|
||||||
|
timerConfig = {
|
||||||
|
OnCalendar = "daily";
|
||||||
|
Persistent = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
./openssh.nix
|
./openssh.nix
|
||||||
../../services/prometheus-node.nix
|
../../services/prometheus-node.nix
|
||||||
./nginx.nix
|
./nginx.nix
|
||||||
|
./dav2ical.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
networking = {
|
networking = {
|
||||||
|
|
|
||||||
BIN
secrets/caldav-export-config.age
Normal file
BIN
secrets/caldav-export-config.age
Normal file
Binary file not shown.
|
|
@ -65,4 +65,5 @@ in
|
||||||
];
|
];
|
||||||
"www-staging-htpasswd.age".publicKeys = xengi ++ [ _www ];
|
"www-staging-htpasswd.age".publicKeys = xengi ++ [ _www ];
|
||||||
"radicale_htpasswd.age".publicKeys = xengi ++ [ _dav ];
|
"radicale_htpasswd.age".publicKeys = xengi ++ [ _dav ];
|
||||||
|
"caldav-export-config.age".publicKeys = xengi ++ [ _dav ];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue