Sync WebCal and CalDAV directly; add a skip_past cutoff
WebCal is calcalist's own (src/webcal.rs), with Calendar::split_by_uid to turn one feed document into one file per UID. Shared crate::http and crate::secret came out of google/ so every leg uses them. CalDAV follows, behind a per-endpoint 'native' flag so it can run beside pimsync one collection at a time: src/caldav/client.rs speaks PROPFIND, sync-collection, calendar-multiget and conditional PUT/DELETE, and src/caldav/mod.rs is the pull/push pair. Radicale answers an initial sync-collection with no members, so a cursor is only stored alongside a complete read. doctor reports what a server advertises. skip_past (src/horizon.rs) takes a date, not a flag: a rolling 'before now' would retire events from the aggregate as they aged, deleting yesterday's events on every cycle. Pinned to a date, what is mirrored stays mirrored. On the real configuration it leaves 2600 of 3282 events behind. The filter sits above the prior-links lookup in mirror_source, which is what makes it safe: from there DeleteSource is unreachable, so excluding an event can never be read as the user having deleted its mirror. pimsync: CWD pinned so vstorage's relative unlink cannot reach the user's files; the dry-run workspace is removed by Drop rather than by a line every '?' stepped over. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d03748472a
commit
622a71dcd6
28 changed files with 2717 additions and 188 deletions
16
README.md
16
README.md
|
|
@ -25,7 +25,7 @@ Or straight from the repo, without cloning it first:
|
|||
nix profile install "git+https://code.randogoth.com/randogoth/CalCalist.git#calcalist"
|
||||
```
|
||||
|
||||
One runtime dependency is needed for CalDAV and iCal sync: **`pimsync`, from 0.5.11 up to but not including 0.7.0, on `PATH`**. The Nix flake installation method already ensures this: it builds pimsync from source and wraps `calcalist` so that copy is the one found, whatever else is installed.
|
||||
One runtime dependency is needed for CalDAV endpoints that are not `native`, and for `calcalist discover`: **`pimsync`, from 0.5.11 up to but not including 0.7.0, on `PATH`**. Google calendars, iCal feeds and `native` CalDAV collections need nothing extra — calcalist syncs those itself, so a configuration whose CalDAV endpoints are all `native` never calls pimsync during a cycle. The Nix flake installation method already ensures this: it builds pimsync from source and wraps `calcalist` so that copy is the one found, whatever else is installed.
|
||||
|
||||
To run a different pimsync — a fork, or a working tree — override the input at install time:
|
||||
|
||||
|
|
@ -64,10 +64,13 @@ type = "caldav"
|
|||
url = "https://posteo.de:8443/calendars/you/abc123/work/"
|
||||
username = "you@posteo.de"
|
||||
secret_command = "secret-tool lookup service posteo user you@posteo.de"
|
||||
native = true # optional; sync this collection with calcalist's own client
|
||||
```
|
||||
|
||||
`url` must name the calendar collection itself, not the server root.
|
||||
|
||||
`native = true` syncs the collection with calcalist's own CalDAV client instead of pimsync, so that endpoint needs no external binary and reports its own failures rather than taking the whole CalDAV leg down with it. It is per-endpoint so the two can run side by side, one collection each, while the native client earns the default. It is developed and tested against [Radicale](https://radicale.org/); other CalDAV servers are untested rather than unsupported, and `calcalist doctor` prints what each server advertises so an unfamiliar one fails legibly.
|
||||
|
||||
Rather than digging it out of a provider's web interface, ask the server — this prints a ready-to-paste block per calendar, with ids taken from the URL for you to rename:
|
||||
|
||||
```sh
|
||||
|
|
@ -115,6 +118,7 @@ default_sink = "gcal"
|
|||
conflict = "source_wins" # the only policy; the origin calendar wins
|
||||
propagate_deletes = true # deleting a mirror deletes the original
|
||||
max_delete_fraction = 0.2 # abort if more of the aggregate than this vanishes
|
||||
# skip_past = "2026-01-01" # leave finished, non-repeating events from before this out
|
||||
```
|
||||
|
||||
The rules, all checked before anything is synchronised, with every problem reported in one pass:
|
||||
|
|
@ -211,6 +215,7 @@ A category matching an endpoint id works too. Note that *any* category on a new
|
|||
|
||||
## What it does to your calendars
|
||||
|
||||
- **`skip_past` leaves history behind.** Give it a date — `skip_past = "2026-01-01"` — and an event is left out when it ended before that date *and* carries no `RRULE` or `RDATE`, so a series that began years ago still travels; its next occurrence is the point. An event whose dates cannot be read is kept rather than guessed at. This is what makes a first sync carry only what is still to come instead of publishing years of finished events into a calendar you subscribe to; on one real configuration it was 79% of them. A fixed date rather than a rolling "before now" is deliberate: a moving cutoff would retire events from the aggregate as they aged, so the published calendar would quietly delete yesterday's events on every cycle. `calcalist sync --skip-past [DATE]` applies one for a single cycle whatever the file says, defaulting to today when given bare — useful with `--dry-run` for seeing what a cutoff would leave out. Excluded events are never deleted from their source calendar; only the aggregate's copy is affected. Applying a cutoff to an aggregate that already carries history retires those mirrors, which can trip the mass-deletion guard and need `--force` the first time.
|
||||
- **Deleting an event in the aggregate deletes it at its source**, unless `propagate_deletes = false`. A cycle that would remove more than `max_delete_fraction` of the aggregate is refused until you pass `--force` — with a floor of three, so removing one or two events is never blocked.
|
||||
- **Edited in both places since the last sync, the source wins.** The conflict is reported.
|
||||
- **Alarms are always kept**, with no option to strip them. The tool exists so you subscribe to one calendar rather than several, so dropping reminders would disarm every one you have.
|
||||
|
|
@ -233,7 +238,7 @@ systemctl --user daemon-reload
|
|||
systemctl --user enable --now calcalist.timer
|
||||
```
|
||||
|
||||
The service expects the binary at `~/.local/bin/calcalist`; edit `ExecStart` if yours is elsewhere. Only one cycle runs at a time — a timer firing while you are running `sync` by hand is refused, naming the process that holds the lock. A cycle that could not reach an endpoint exits non-zero on purpose, so a lapsed token shows up as a failed unit rather than passing unnoticed; `journalctl --user -u calcalist` has the report. If your secrets come from a
|
||||
The service expects the binary at `~/.nix-profile/bin/calcalist`, where `nix profile install` puts it; edit `ExecStart` if yours is elsewhere — a `cargo install` lands in `~/.cargo/bin`. The path is absolute because a user manager's `PATH` is not the login shell's. Only one cycle runs at a time — a timer firing while you are running `sync` by hand is refused, naming the process that holds the lock. A cycle that could not reach an endpoint exits non-zero on purpose, so a lapsed token shows up as a failed unit rather than passing unnoticed; `journalctl --user -u calcalist` has the report. If your secrets come from a
|
||||
keyring needing an unlocked session, the timer only works while you are logged in.
|
||||
|
||||
## Commands
|
||||
|
|
@ -259,7 +264,7 @@ Everything machine-local lives under `$XDG_STATE_HOME/calcalist`:
|
|||
```
|
||||
state.json event mappings, content hashes, sync cursors
|
||||
lock held for the duration of a cycle
|
||||
pimsync.conf generated on every sync; edits are overwritten
|
||||
pimsync.conf generated when pimsync is used; edits are overwritten
|
||||
pimsync-status/ pimsync's own record of what it has seen
|
||||
vdir/<endpoint-id>/ the local mirror of each endpoint
|
||||
google/accounts/<account>.json refresh tokens, 0600
|
||||
|
|
@ -270,8 +275,9 @@ configuration between machines; leave this behind.
|
|||
|
||||
## Known gaps
|
||||
|
||||
- **A failing `pimsync sync` stops the whole CalDAV leg.** Google endpoints are isolated from each other, but pimsync is a single process covering every
|
||||
CalDAV and feed pair, and a failure does not say which pair it belongs to.
|
||||
- **A failing `pimsync sync` stops every CalDAV endpoint that is not `native`.** Google, feed and `native` CalDAV endpoints are isolated from each other
|
||||
and from this, but pimsync is a single process covering every pair it still owns, and a failure does not say which pair it belongs to. Setting
|
||||
`native = true` on those endpoints removes them from its reach, and with it the gap.
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue