File versioning: design ADR, per-driver version storage, restore/download UI (#530, 4/4) #541

Closed
opened 2026-09-14 02:48:24 +00:00 by Cordy · 3 comments
Owner

Fourth and heaviest slice of #530. Per-file version history with Restore and Download per version, as in the OpenCloud reference.

ADR first — this is a design problem before it is a feature. The design must answer:

  • Version storage per driver: posix layout vs s3/Garage (no rename on Garage; single PUT atomicity — same constraints as #153).
  • Encryption: every version is its own age file; recovery and retained-key rotation apply to old versions too; key custody outages must not orphan version history (see #528).
  • Quota: do versions count against the user/space quota, and how is that surfaced?
  • Legal holds: a hold must freeze the version chain — a version delete or GC under hold is the same class of bug as the silent hold release found in #136/#138.
  • Retention/GC: how many versions, for how long, who configures it (admin setting), what the default is.
  • Write paths: web upload, WebDAV PUT, WOPI PutFile (office edits autosave frequently — needs coalescing or office edits will mint hundreds of versions), copy/rename semantics.
  • Trash interplay: deleting a file with versions, restoring it.

Then: backend TDD (version list/read/restore APIs), and the Versions panel in #538's details pane (list with per-entry Restore/Download). Restore writes an activity event (#539).

Fourth and heaviest slice of #530. Per-file version history with Restore and Download per version, as in the OpenCloud reference. **ADR first — this is a design problem before it is a feature.** The design must answer: - Version storage per driver: posix layout vs s3/Garage (no rename on Garage; single PUT atomicity — same constraints as #153). - Encryption: every version is its own age file; recovery and retained-key rotation apply to old versions too; key custody outages must not orphan version history (see #528). - Quota: do versions count against the user/space quota, and how is that surfaced? - Legal holds: a hold must freeze the version chain — a version delete or GC under hold is the same class of bug as the silent hold release found in #136/#138. - Retention/GC: how many versions, for how long, who configures it (admin setting), what the default is. - Write paths: web upload, WebDAV PUT, WOPI PutFile (office edits autosave frequently — needs coalescing or office edits will mint hundreds of versions), copy/rename semantics. - Trash interplay: deleting a file with versions, restoring it. Then: backend TDD (version list/read/restore APIs), and the Versions panel in #538's details pane (list with per-entry Restore/Download). Restore writes an activity event (#539).
Author
Owner

ADR drafted: PR #611 (VERSIONING.md on branch adr-541, deliberately unmerged). Core proposal: a version is an immutable byte-copy of the previous ciphertext, minted on overwrite — no renames (Garage-safe), no metadata index (the storage listing IS the version list, nothing to corrupt), keyscan/keydrain extended so rotation never strands old versions, versions bill quota, holds freeze the chain while restore stays allowed (restore mints first, so it only ever adds), mint-time GC (10 versions / 90 days defaults), WOPI coalescing (10 min) so office autosave can't mint hundreds. Four open questions at the bottom of the doc need your answers before the build.

ADR drafted: **PR #611** (`VERSIONING.md` on branch `adr-541`, deliberately unmerged). Core proposal: a version is an immutable byte-copy of the previous ciphertext, minted on overwrite — no renames (Garage-safe), no metadata index (the storage listing IS the version list, nothing to corrupt), keyscan/keydrain extended so rotation never strands old versions, versions bill quota, holds freeze the chain while restore stays allowed (restore mints first, so it only ever adds), mint-time GC (10 versions / 90 days defaults), WOPI coalescing (10 min) so office autosave can't mint hundreds. Four open questions at the bottom of the doc need your answers before the build.
Author
Owner

ADR accepted and merged (PR #611VERSIONING.md). All four questions resolved at review: defaults confirmed (10 / 90 days / 10-min coalescing); recipient surface hidden in v1; quota rewritten to the Nextcloud-style model per Nikola — versions invisible to the user-visible quota, retention settings as the true billing lever, admin sees real usage, 50%-of-free-space valve documented as the v2 tightening; same-content overwrites mint nothing. The issue stays open for the build (backend TDD + Versions panel in #538's shell), which follows the #538/#539/#540 work.

**ADR accepted and merged** (PR #611 → `VERSIONING.md`). All four questions resolved at review: defaults confirmed (10 / 90 days / 10-min coalescing); recipient surface hidden in v1; **quota rewritten to the Nextcloud-style model per Nikola** — versions invisible to the user-visible quota, retention settings as the true billing lever, admin sees real usage, 50%-of-free-space valve documented as the v2 tightening; same-content overwrites mint nothing. The issue stays open for the build (backend TDD + Versions panel in #538's shell), which follows the #538/#539/#540 work.
Author
Owner

Shipped in v0.6.223 (PR #622, tag on 8bf73b3), live on both dogfoods. VERSIONING.md gained an "Implementation notes (v1)" section recording the two mechanism deviations.

What landed, against the ADR sections:

  • §1/§2: internal/storage/versions — mint-on-overwrite byte-copies of the previous raw object (ciphertext) under <dir>/.cairn-versions/<basename>/<rfc3339>_<seq>_<author>.v. Mint happens before the write, so a crash leaves an extra version, never a lost file. Author rides the key (deviation #2 — §1 forbids an index and §9's listing + §7's coalescing both need an author).
  • §2 guard (deviation #1): namespace hidden at the versioning decorator, not reserved — reserved sits below encrypt, where version blobs must stay reachable so downloads decrypt with the file's own path-scoped identity (the version dir shares the file's prefix). Same behavior: never listed, direct access = not-found, uploads refused; internal ctx marker for the API.
  • §3: keyscan/keydrain walk baseStore (raw) already — version blobs are covered by rotation with zero changes. Custody outage: unreadable, never deleted (nothing version-specific to do).
  • §4: decorator sits between encrypt and quota — quota enforcement and the per-user usage walk read through its filtered listings, so versions are invisible to the user-visible number; the admin raw-layer sizes include them (correct: operator sees real bytes). The dedicated "of which versions" line is deferred (needs a per-owner raw prefix walk).
  • §5: Held hook from the holds store freezes prune/coalesce/unmint/chain-drop.
  • §6: 10/90/10 defaults as settings (versionsMaxPerFile, versionsMaxAgeDays with -1 = forever, versionsCoalesceMinutes), read live, enforced at mint; age cap lazy at list + permanent at next mint. No background sweeper.
  • §7: all write paths covered (the decorator sees web, WebDAV, TUS, peering delivery); WOPI PutFile sets the coalesce ctx marker; same-content skip via sha256 tee against the old plaintext; copies start empty; file renames carry the chain through the inner encrypt-aware Rename (cross-scope moves re-encrypt versions like the file itself).
  • §8: free by construction — trash deletes/restores are renames through the stack, so the chain follows to the trash and back; purge (inner.Delete) drops it under the existing hold/retention checks.
  • §9: the three endpoints, Stat-gated, /shared refused; restore mints current first, emits version-restore audit (registered) + activity event. Versions panel in the pane: time · author · size, Download + Restore per row, restore hidden on read-only items.

v1 edges to know while dogfooding:

  1. Restore is own-home only — the raw-layer restore bypasses scope/hold (hold bypass is ADR-sanctioned), so it's fenced to /home/<you>/. Space restores are a follow-up.
  2. Restore doesn't nudge sync clients (#478 changes journal not written on the raw path) — a synced copy catches up on its next real change or poll.
  3. Listed sizes are ciphertext sizes (small age overhead) — same stance as admin billing.
  4. Every overwrite now costs one extra read of the old content (the same-content check decrypts and hashes it). Fine at dogfood scale; flagged for the beta if large files hurt.

Eyeball: overwrite a file twice (upload the same name, or edit in Collabora), then Details → Versions — restore the older one and watch it mint the undo version first.

Shipped in **v0.6.223** (PR #622, tag on 8bf73b3), live on both dogfoods. VERSIONING.md gained an "Implementation notes (v1)" section recording the two mechanism deviations. **What landed, against the ADR sections:** - §1/§2: `internal/storage/versions` — mint-on-overwrite byte-copies of the previous raw object (ciphertext) under `<dir>/.cairn-versions/<basename>/<rfc3339>_<seq>_<author>.v`. Mint happens before the write, so a crash leaves an extra version, never a lost file. Author rides the key (deviation #2 — §1 forbids an index and §9's listing + §7's coalescing both need an author). - §2 guard (deviation #1): namespace hidden at the versioning decorator, not `reserved` — reserved sits below encrypt, where version blobs must stay reachable so downloads decrypt with the file's own path-scoped identity (the version dir shares the file's prefix). Same behavior: never listed, direct access = not-found, uploads refused; internal ctx marker for the API. - §3: keyscan/keydrain walk `baseStore` (raw) already — version blobs are covered by rotation with zero changes. Custody outage: unreadable, never deleted (nothing version-specific to do). - §4: decorator sits between encrypt and quota — quota enforcement and the per-user usage walk read through its filtered listings, so versions are invisible to the user-visible number; the admin raw-layer sizes include them (correct: operator sees real bytes). The dedicated "of which versions" line is **deferred** (needs a per-owner raw prefix walk). - §5: `Held` hook from the holds store freezes prune/coalesce/unmint/chain-drop. - §6: 10/90/10 defaults as settings (`versionsMaxPerFile`, `versionsMaxAgeDays` with -1 = forever, `versionsCoalesceMinutes`), read live, enforced at mint; age cap lazy at list + permanent at next mint. No background sweeper. - §7: all write paths covered (the decorator sees web, WebDAV, TUS, peering delivery); WOPI PutFile sets the coalesce ctx marker; same-content skip via sha256 tee against the old plaintext; copies start empty; file renames carry the chain through the inner encrypt-aware Rename (cross-scope moves re-encrypt versions like the file itself). - §8: free by construction — trash deletes/restores are renames through the stack, so the chain follows to the trash and back; purge (`inner.Delete`) drops it under the existing hold/retention checks. - §9: the three endpoints, Stat-gated, `/shared` refused; restore mints current first, emits `version-restore` audit (registered) + activity event. Versions panel in the pane: time · author · size, Download + Restore per row, restore hidden on read-only items. **v1 edges to know while dogfooding:** 1. **Restore is own-home only** — the raw-layer restore bypasses scope/hold (hold bypass is ADR-sanctioned), so it's fenced to `/home/<you>/`. Space restores are a follow-up. 2. **Restore doesn't nudge sync clients** (#478 changes journal not written on the raw path) — a synced copy catches up on its next real change or poll. 3. Listed sizes are ciphertext sizes (small age overhead) — same stance as admin billing. 4. Every overwrite now costs one extra read of the old content (the same-content check decrypts and hashes it). Fine at dogfood scale; flagged for the beta if large files hurt. Eyeball: overwrite a file twice (upload the same name, or edit in Collabora), then Details → Versions — restore the older one and watch it mint the undo version first.
Cordy closed this issue 2026-09-18 18:05:13 +00:00
Sign in to join this conversation.
No labels
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Cordy/Cairn#541
No description provided.