crabidy/architecture/capture-deletion.md

99 lines
4.1 KiB
Markdown

# Capture deletion
Deleting under `/captures` reclaims disk: downloaded audio is the one
library content that is expensive to recreate (slow, throttled downloads
— architecture/youtube-rustypipe.md), so stale captures must be
removable from the TUI, and removal must actually delete the files.
## Context
Before this feature, deletion (`d`) was limited to *top-level* folders
of editable fsdy instances (architecture/bookmarks.md D4): whole
captures could be deleted (and were removed from disk via
`remove_dir_all`), but nothing below — no single album, no single
track. Deletes were deliberately unconfirmed because every deletable
node was cheap to recreate.
## Decisions
### D1 — deletable tree as an fsdy instance option
`fsdy::Client::with_deletable_tree()` makes every folder below the
instance root deletable (recursively, any depth) and every track file
deletable. Only the `/captures` instance sets it:
- `/queues` and `/bookmarks` keep the top-level-only contract; their
nested structure mirrors a snapshot and partial edits are better done
by re-saving.
- Nested folders become deletable but **not renamable** — renames would
break the incremental-capture merge by name
(architecture/incremental-captures.md), deletes cannot: a re-capture
of the same name simply re-downloads what is missing.
- The instance root itself and reserved top-level names stay
undeletable even on a deletable tree.
### D2 — tracks advertise deletability through their node
Wire truth, not client guessing: the TUI must not hardcode which tracks
are deletable. But a per-`Track` flag would touch every Track literal
in every provider for a capability only fsdy uses. Instead
`LibraryNode.tracks_deletable` says "this node's listed tracks may be
deleted", exactly like the existing `is_downloadable` inheritance
("tracks inherit their node's blessing", architecture/captures.md D4).
Child folders keep using the existing per-child `is_deletable`.
### D3 — a deleted track takes its audio with it, inside the root only
Deleting a track file removes the `.cbd-track.toml` **and** the audio
its `[playable] file` points to — that is the point of the feature.
Safety boundary: the audio path (relative values resolved against the
track file's directory) is canonicalized and must live inside the
canonicalized instance root; anything else is kept and logged. So a
hand-written track file referencing `~/Music/song.flac` from inside the
captures folder can never delete foreign data, and `..`/symlink tricks
resolve before the check. A track file that no longer parses is deleted
blind (its audio cannot be located; the listing skipped it anyway).
Deletes stay idempotent per the proto contract.
### D4 — confirmation in the client, scoped to /captures
`d` on anything under `/captures` opens a one-line modal prompt
(`delete <title>? [y/N]`, red) instead of sending; only `y`/`Y`
confirms, any other key cancels. Every other deletable (search terms,
bookmarks, saved queues) stays a single unconfirmed keypress
(architecture/node-editing.md D4) — they are cheap to recreate, and a
blanket confirmation would train reflexive `y`. The scoping is a path
check in the TUI (like the `/captures` never-cache rule in
`cbd-tui/src/rpc.rs`): the server does not know which deletes a client
should consider expensive.
## Flow
```d2
direction: right
tui: cbd-tui {
d: "d on /captures/…"
confirm: "delete …? [y/N]"
d -> confirm
}
server: crabidy-server {
provider_loop: provider loop
}
fsdy: fsdy /captures instance {
folder: "folder: remove_dir_all"
track: "track: toml + contained audio"
}
tui.confirm -> server.provider_loop: y → DeleteLibraryNode
server.provider_loop -> fsdy.folder
server.provider_loop -> fsdy.track
fsdy.folder -> tui: refreshed parent listing
```
## Risks / notes
- The confirmation prompt occupies the same line as the text-input
overlay; both are strictly modal and never open together.
- Deleting the folder of a *running* capture is possible; the capture
walk recreates directories as it goes and re-downloads on the next
run, so the race wastes bandwidth but corrupts nothing.