Trash / file versioning — strategic spike #23

Closed
opened 2026-07-25 16:26:02 +00:00 by Cordy · 1 comment
Owner

Nextcloud/ownCloud offer a trash bin and file version history. Both are parity expectations but brush hard against the "storage backend is the source of truth / no DB index" non-goal.

Spike:

  • Trash: could be a per-user .trash/ prefix move (no DB) — feasible.
  • Versioning: harder without a metadata store; evaluate ZFS snapshots delegation vs an app-layer scheme vs declaring it out of scope.
  • Output: decision record + scope call.
Nextcloud/ownCloud offer a trash bin and file version history. Both are parity expectations but brush hard against the "storage backend is the source of truth / no DB index" non-goal. Spike: - Trash: could be a per-user `.trash/` prefix move (no DB) — feasible. - Versioning: harder without a metadata store; evaluate ZFS snapshots delegation vs an app-layer scheme vs declaring it out of scope. - Output: decision record + scope call.
Author
Owner

Decision record (closes the spike):

Trash — implemented (PR #38). Per-user /.trash/<username>/ prefix move with a JSON sidecar per entry; backend stays the single source of truth, no DB index. REST surface: GET /api/v1/trash, POST /api/v1/trash/{id}/restore, DELETE /api/v1/trash/{id}. Opt-in via storage.trash.enabled. Hidden from non-admins on the raw storage surface; admin deletes inside /.trash are real (that's the "empty trash" path). Retention/auto-purge deferred — no background jobs in cairnd yet; revisit if trash growth becomes real.

Versioning — delegated to the storage layer, out of scope for the app (v0.4 and likely beyond). Rationale:

  • App-layer versioning without a metadata store means either a sidecar-per-version scheme (unbounded write amplification, hard rename/copy semantics) or a DB index — the exact non-goal §2 rejects.
  • Our deployment layer already does this better: ZFS snapshots on TrueNAS for POSIX roots, bucket versioning in Garage/any S3 for the s3 driver. Both give point-in-time recovery with zero app complexity and survive app bugs (which app-layer versioning does not).
  • Documented stance: "Cairn protects against user mistakes with trash; history is the storage backend's job." If a hosted/appliance product later needs built-in history, revisit with a real metadata-store decision, not as a bolt-on.
**Decision record (closes the spike):** **Trash — implemented** (PR #38). Per-user `/.trash/<username>/` prefix move with a JSON sidecar per entry; backend stays the single source of truth, no DB index. REST surface: `GET /api/v1/trash`, `POST /api/v1/trash/{id}/restore`, `DELETE /api/v1/trash/{id}`. Opt-in via `storage.trash.enabled`. Hidden from non-admins on the raw storage surface; admin deletes inside `/.trash` are real (that's the "empty trash" path). Retention/auto-purge deferred — no background jobs in cairnd yet; revisit if trash growth becomes real. **Versioning — delegated to the storage layer, out of scope for the app (v0.4 and likely beyond).** Rationale: - App-layer versioning without a metadata store means either a sidecar-per-version scheme (unbounded write amplification, hard rename/copy semantics) or a DB index — the exact non-goal §2 rejects. - Our deployment layer already does this better: ZFS snapshots on TrueNAS for POSIX roots, bucket versioning in Garage/any S3 for the s3 driver. Both give point-in-time recovery with zero app complexity and survive app bugs (which app-layer versioning does not). - Documented stance: "Cairn protects against *user* mistakes with trash; *history* is the storage backend's job." If a hosted/appliance product later needs built-in history, revisit with a real metadata-store decision, not as a bolt-on.
Cordy closed this issue 2026-07-26 14:26:16 +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#23
No description provided.