103 lines
4.3 KiB
Markdown
103 lines
4.3 KiB
Markdown
# Capture deletion
|
|
|
|
> **Superseded** by `crabidy-store.md`: audio now lives in a shared store that
|
|
> track deletion never touches, so deletion on `/crabidy` goes through directly
|
|
> with no confirmation and no disk reclamation. This whole feature is removed.
|
|
|
|
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.
|