Files

4.8 KiB

Design

Context

See proposal.md - Why. Relevant current state:

  • photos.Service.PermanentDelete (backend/internal/photos/service.go:592) resolves authorization via albums.Access(albumID, userID) before HardDelete. A photo whose album was deleted has album_id = NULL; Access("") returns album_not_found, so the delete can never proceed. Restore already special-cases the missing album (album_required), but PermanentDelete does not.
  • The trash listing (photos.Store.ListTrash) is capped at LIMIT 200 for display, while TrashCount is uncapped. A bulk purge must not silently stop at 200.
  • Object cleanup after HardDelete is per-photo: PhotoObjectIDs then MarkObjectDeleting for orphans, with a background worker removing blobs.
  • The trash screen (frontend/src/routes/(app)/trash/+page.svelte) has per-photo actions only, and ConfirmDialog is already available for confirmations.

Goals / Non-Goals

Goals:

  • Permanently deleting an albumless trashed photo works for its uploader.
  • One action empties the trash for everything the caller may delete, regardless of the 200-item display cap, and reports the number removed.
  • Consistent object garbage collection for every removed photo.

Non-Goals:

  • Trash retention/auto-expiry policies.
  • Changing trash visibility rules (ListTrash) or the 200-item display cap.
  • Set-based SQL bulk deletion in this change.

Decisions

Authorize albumless permanent deletion by uploader

When p.AlbumID == "", PermanentDelete SHALL fall back to p.UploaderID == userID for authorization instead of calling albums.Access. When an album is present, the existing album-role check is unchanged. This mirrors Restore, which already treats a missing album as a distinct path, and keeps the permission model identical: only a photo's uploader can remove a photo that no longer belongs to an album.

  • Alternative considered: require the client to always supply a target album for delete. Rejected - permanent deletion has no target album, and the trash legitimately holds albumless photos.

Empty trash as a server-side operation on the trash resource

Add DELETE /trash returning { "deleted": <n> }, backed by photos.Service.EmptyTrash(ctx, userID). It iterates the caller's accessible trashed photos and calls the existing PermanentDelete for each, counting successes and skipping photos the caller may not delete. Reusing PermanentDelete keeps authorization and object GC identical to single-photo deletion.

  • Alternative considered: client enumerates trash and calls POST /photo-batch/... with every id. Rejected - the display list is capped at 200 and this is racy; the server already knows the full accessible set.
  • Alternative considered: a single DELETE FROM photos ... WHERE <authorization>. Rejected for now - album-role authorization lives in Go (roles.CanDelete*), and per-photo object GC is required anyway.

Bypass the 200-item display cap for purging

EmptyTrash must see more than the display page. Add a store method that returns all accessible trashed photo identities (id, album_id, uploader_id) with no display cap, and authorize each in the service. This avoids looping on a capped list (which would also risk an infinite loop when some items are not deletable).

  • Alternative considered: paginate ListTrash until a pass removes nothing. Rejected - undeletable items would reappear and a zero-progress pass is needed to stop; the uncapped identity listing is simpler and bounded by the user's own trash size.

Frontend: explicit action plus confirmation

Add an «Очистить корзину» control on the trash screen header, enabled when the list is non-empty. It opens the existing ConfirmDialog; on confirm it calls DELETE /trash, then reloads the list and refreshes trash store count. Show a busy state while running.

Risks / Trade-offs

  • [Large trash makes the per-photo loop slow] -> Each PermanentDelete is a small fixed number of queries; bulk deletes are O(n) round trips. Acceptable for now; a set-based purge can replace the loop later without changing the spec behavior. Keep the frontend busy state to make latency legible.
  • [Uncapped listing memory] -> The query returns only the caller's trashed photo identities; bounded in practice by trash size. Revisit with chunking if it grows.
  • [Partial failure mid-purge] -> EmptyTrash skips failures and returns the count deleted; the remaining items stay in the trash and can be retried.
  • [Albumless deletes and realtime] -> PermanentDelete only publishes when AlbumID != ""; albumless removals publish nothing, which is correct since no album is watching.

Migration Plan

No schema change. Deploy backend and frontend together; the new endpoint is additive and the authorizaton fix is behavior-only. Rollback is reverting the deploy.

Open Questions

None blocking.