Bis hierher holte jeder Durchgang die volle Terminliste: ctag abfragen, Aenderung sehen, alles neu laden. Eine verschobene Besprechung kostete damit so viel wie ein Neuanfang -- bei einem Jahr Termine, alle fuenfzehn Minuten, auf Akku und Datenvolumen des Telefons. Kein zweiter Zaehler: Das ctag **ist** bereits eine Zahl je Kalender, die bei jedem Schreiben um eins steigt. Jedes Objekt merkt sich in sync_seq den Stand, bei dem es zuletzt angefasst wurde (Migration 24). Grabsteine (cal_tombstones) halten geloeschte UIDs fest -- ohne sie bliebe ein geloeschter Termin auf jedem Geraet stehen, das gerade nicht verbunden war. Sie stehen 180 Tage; beim Raeumen wandert sync_floor mit, und ein Token darunter bekommt 403 <valid-sync-token/> statt einer Antwort mit fehlenden Loeschungen. go-webdav kennt fuer REPORT nur calendar-query und calendar-multiget, und weder sync-token noch supported-report-set. Beide gehen denselben Weg wie getctag und calendar-color. Ohne supported-report-set probiert DAVx5 sync-collection gar nicht erst aus. Zwei Fehler noch vor dem Ausliefern gefunden: * Die Anfuehrungszeichen am ETag standen **innerhalb** der Maskierung -- xml.EscapeText machte daraus ". Gueltig, aber genug Clients vergleichen ETags roh und saehen bei jedem Durchgang eine Aenderung. * pruneTombstones nahm die erste Zeile eines DELETE ... RETURNING als Maximum. Das hat keine Reihenfolge -- der sync_floor waere zu niedrig gesetzt worden, und ein Client dazwischen haette Loeschungen verpasst. caldav-selbsttest.py: 62 -> 86 Pruefungen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
64 lines
2.4 KiB
Go
64 lines
2.4 KiB
Go
// Package calendar ist ein eigenständiges Modul, kein Kernbestandteil:
|
||
// CalDAV für Termine und Aufgaben. Es importiert nichts aus files, sharing
|
||
// oder admin – nur auth und acl, wie jeder andere Bereich auch. Ist es
|
||
// abgeschaltet, gibt es keine Route und keinen Menüpunkt; die Tabellen
|
||
// bleiben dann einfach ungenutzt liegen. Siehe kalender-plan.md.
|
||
package calendar
|
||
|
||
import (
|
||
"time"
|
||
|
||
"github.com/google/uuid"
|
||
)
|
||
|
||
// Calendar ist eine Sammlung von Terminen/Aufgaben – das, was ein Client als
|
||
// einen Kalender in seiner Liste zeigt.
|
||
type Calendar struct {
|
||
ID uuid.UUID `json:"id"`
|
||
OwnerID uuid.UUID `json:"owner_id"`
|
||
Name string `json:"name"`
|
||
Color string `json:"color"`
|
||
Description string `json:"description"`
|
||
SupportsTodo bool `json:"supports_todo"`
|
||
// TimeZone ist die Vorgabe für neue Termine, die die Web-Oberfläche
|
||
// künftig hier anlegt (Phase 3). CalDAV selbst braucht sie nicht –
|
||
// jeder Termin trägt seine Zeitzone bereits im .ics.
|
||
TimeZone string `json:"timezone"`
|
||
// PathSegment ist die Adresse, unter der ein Client diesen Kalender
|
||
// angelegt hat ("…/calendars/max/arbeit/"). Leer heißt: keine gewählte
|
||
// Adresse, es gilt die UUID. Siehe Ref.
|
||
PathSegment string `json:"-"`
|
||
CTag int64 `json:"ctag"`
|
||
// SyncFloor: Bis zu diesem Stand sind Grabsteine weggeräumt. Ein Token
|
||
// darunter lässt sich nicht mehr vollständig beantworten – der Client
|
||
// bekommt dann eine Absage und gleicht vollständig ab. Siehe
|
||
// Store.ChangesSince.
|
||
SyncFloor int64 `json:"-"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
}
|
||
|
||
// Ref ist das Pfadsegment, unter dem dieser Kalender in CalDAV erreichbar
|
||
// ist: die vom Client gewählte Adresse, sonst die UUID.
|
||
func (c *Calendar) Ref() string {
|
||
if c.PathSegment != "" {
|
||
return c.PathSegment
|
||
}
|
||
return c.ID.String()
|
||
}
|
||
|
||
// Object ist ein einzelner Termin oder eine Aufgabe. Data ist die
|
||
// vollständige .ics-Datei, unverändert wie vom Client geschickt – siehe
|
||
// kalender-plan.md, Abschnitt "Felder".
|
||
type Object struct {
|
||
ID uuid.UUID `json:"id"`
|
||
CalendarID uuid.UUID `json:"calendar_id"`
|
||
UID string `json:"uid"`
|
||
Component string `json:"component"` // "VEVENT" | "VTODO"
|
||
Data string `json:"-"`
|
||
ETag string `json:"etag"`
|
||
DTStart *time.Time `json:"dtstart,omitempty"`
|
||
DTEnd *time.Time `json:"dtend,omitempty"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
UpdatedAt time.Time `json:"updated_at"`
|
||
}
|