Trash: recoverable deletes via /.trash/<user>/ (v0.4 #23) #38

Merged
Cordy merged 4 commits from feat/trash into main 2026-07-26 14:25:53 +00:00
Owner

Implements the trash half of issue #23 (the feasible "prefix move, no DB" path from the spike).

Decorator (internal/storage/trash, sits below the scope overlay): Delete moves the item to /.trash/<username>/<id>.data with a JSON sidecar <id>.meta {path, name, size, isDir, deletedAt} — the backend remains the single source of truth, no database index. Admin deletes inside /.trash are real deletes (that's how an admin empties trash); requests with no user context delete for real.

API (registered when storage.trash.enabled):

  • GET /api/v1/trash — own entries, newest first
  • POST /api/v1/trash/{id}/restore — restore to original path (recreates missing parents; 409 if the path is now occupied)
  • DELETE /api/v1/trash/{id} — purge permanently

Hiding: /.trash is invisible/untouchable to non-admins on the raw storage surface (List filters it at root; Stat/Open/Write/Mkdir/Rename/Copy return 404). With perUserHomes on, the scope overlay already denies it as an unknown top level.

Config: storage.trash.enabled (default off). POSIX backend recommended — S3 directory rename is copy-heavy.

Tests use a real in-memory backend: delete→trash move, restore (incl. vanished parent dirs), restore conflict, purge, non-admin hiding + admin real-delete, no-user passthrough, directory round-trip.

Versioning (other half of #23) gets a decision record on the issue: delegated to backend snapshots, out of scope for v0.4.

Implements the trash half of issue #23 (the feasible "prefix move, no DB" path from the spike). **Decorator** (`internal/storage/trash`, sits below the scope overlay): `Delete` moves the item to `/.trash/<username>/<id>.data` with a JSON sidecar `<id>.meta` `{path, name, size, isDir, deletedAt}` — the backend remains the single source of truth, no database index. Admin deletes inside `/.trash` are real deletes (that's how an admin empties trash); requests with no user context delete for real. **API** (registered when `storage.trash.enabled`): - `GET /api/v1/trash` — own entries, newest first - `POST /api/v1/trash/{id}/restore` — restore to original path (recreates missing parents; 409 if the path is now occupied) - `DELETE /api/v1/trash/{id}` — purge permanently **Hiding:** `/.trash` is invisible/untouchable to non-admins on the raw storage surface (List filters it at root; Stat/Open/Write/Mkdir/Rename/Copy return 404). With `perUserHomes` on, the scope overlay already denies it as an unknown top level. **Config:** `storage.trash.enabled` (default off). POSIX backend recommended — S3 directory rename is copy-heavy. Tests use a real in-memory backend: delete→trash move, restore (incl. vanished parent dirs), restore conflict, purge, non-admin hiding + admin real-delete, no-user passthrough, directory round-trip. Versioning (other half of #23) gets a decision record on the issue: delegated to backend snapshots, out of scope for v0.4.
Cordy merged commit df0e27175b into main 2026-07-26 14:25:53 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
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#38
No description provided.