Six routes poured full media with no monitor record and no byte measurement:
the two native download routes, the Jellyfin-compat download, both ABS file
variants, and the public ABS RSS feed file. They were the last invisible bytes
on the server.
- internal/transfers: a process-local, in-memory registry of active pours.
Bounded (10k entries) with rate-limited "full" warnings, all request-derived
strings normalized and length-clamped (ABS and jellycompat carry no bounded
client name, so those fields are header-derived and untrusted), overflow-safe
byte accumulation, deterministic snapshots, nil-safe throughout. No I/O on any
path, and no persistence: a pour dies with the process, so durable rows would
only need reaping after a crash.
- Reuses the existing playback.SessionMeteredWriter via ServedBytesRecorder
rather than adding a second writer — that writer is where the sendfile
(ReadFrom) and kill-cut (Unwrap) hazards live and both have regressed before.
- Deliberately NOT plumbed through streammonitor/streamenforcer, which are
untouched. Downloads stay off the live-stream path by construction: there is
no type, field or collection through which one can reach the enforcer, so a
download can never be counted against max_streams or trimmed as an over-cap
stream.
- Admin visibility is a sibling `transfers` array on the existing
/admin/nodes/sessions response; the `sessions` array is byte-for-byte
unchanged. Transfers appear only in the unfiltered listing, since a node_id
filter targets an edge and these are process-local.
- Per-pour ids are unique, never the download id: concurrent and repeated
GET/Range requests against one download row are legitimate and would collide.
The id is minted in the handler (where the revocation watcher is armed) and
the registry entry is opened in the service only after file/artifact
resolution succeeds, so a failed auth or lookup never registers a transfer.
- Defer order is an invariant at every call site: End is registered before the
meter's Close so LIFO flushes the tail first. Reversed, a final sub-1MiB flush
lands on an unknown id and is silently lost. Pinned by a test.
No schema change and no migration: downloads.bytes_sent keeps its documented
meaning (a lifecycle marker set to file_size on completion, not a live counter)
and is untouched.
Phase 2 — killing a single download, and a standing per-user download block —
is deliberately deferred. A user revocation already cuts in-flight download
pours; it is a cutoff, so it does not refuse new ones. The unique per-pour id
exists so phase 2 only changes "" to that id at each WatchAndCut site.
Part of the stream monitoring & kill-switch epic.