Backend:
- LocalCalendar gains caldav_published + dav_token + dav_ctag; LocalEvent gains
etag (migrations in main.py). bump_dav() refreshes ctag/etag on every local
event write (create/update/delete/import).
- local_router: PUT /calendars/{id} accepts caldav_published (mints/revokes
token), new POST /calendars/{id}/dav-token/rotate, _cal_dict exposes
caldav_published + caldav_url.
- New dav_router mounted at root (/dav/{token}/...): a minimal two-way CalDAV
server (OPTIONS/PROPFIND/REPORT/GET/PUT/DELETE) reusing ical_io build/parse,
ctag-based change detection. Secret token = auth, no login.
Frontend:
- Settings calendar table: per-local-calendar publish toggle + subscribe URL
with copy and token-rotate; i18n (de/en) and styling.
Note: reverse proxy must allow WebDAV methods (PROPFIND/REPORT/PUT/DELETE).
VALARM/reminders are not round-tripped via CalDAV.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
42 lines
1.3 KiB
Python
42 lines
1.3 KiB
Python
"""Shared helpers for CalDAV publishing of local calendars.
|
|
|
|
Publishing is opt-in per calendar: a published calendar gets a secret
|
|
``dav_token`` and is reachable as a two-way CalDAV collection at
|
|
``/dav/{token}/``. ``dav_ctag`` changes on every event write so clients detect
|
|
changes; each event carries an ``etag`` that changes on write. Rotating the
|
|
token revokes existing subscriptions.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import secrets
|
|
import uuid
|
|
|
|
|
|
def new_token() -> str:
|
|
"""A URL-safe, unguessable token used as the CalDAV collection path."""
|
|
return secrets.token_urlsafe(24)
|
|
|
|
|
|
def new_tag() -> str:
|
|
"""A fresh ctag/etag value."""
|
|
return uuid.uuid4().hex
|
|
|
|
|
|
def bump_dav(cal, event=None) -> None:
|
|
"""Mark a calendar (and optionally an event) as changed for CalDAV clients.
|
|
|
|
Safe to call unconditionally on every local-event write — it only refreshes
|
|
opaque change tags, so unpublished calendars are unaffected.
|
|
"""
|
|
if cal is not None:
|
|
cal.dav_ctag = new_tag()
|
|
if event is not None:
|
|
event.etag = new_tag()
|
|
|
|
|
|
def caldav_url(request, token: str) -> str:
|
|
"""Absolute CalDAV collection URL for a token, based on the request origin."""
|
|
base = str(request.base_url).rstrip("/")
|
|
return f"{base}/dav/{token}/"
|