Import pipeline: probe, hardlink, rename, layout #82
Reference in New Issue
Block a user
Delete Branch "issue/23-import-pipeline"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Implements #23 (DESIGN.md §7.2–§7.4).
A
downloadedgrab is the gap: the daemon probes the torrent's files, picks the feature, re-evaluates the policy with real evidence, and hardlinks it into the library — copy only onEXDEV, staged through a rename so no partial file is ever visible. Attribute tags come fromffprobe; the source tag is the release name's claim, the one thing a file cannot know (§5.6). Hard fails blacklist the release, fail the grab and reopen the gap; the torrent is never moved or deleted (§7.3). Grab selection now skips blacklisted names (§6.3) so the next candidate wins.New:
arr_core::layout(naming, pure),arr_parse::normalise(blacklist key),TransmissionClient::torrent_content(file list),ImportActionon the reconcile lane.Acceptance test produces the exact §7.4 path and asserts the seeding file keeps a link count of two; a DV Profile 5 probe hard-fails end to end.
just cigreen.🤖 Generated with Claude Code
@@ -0,0 +139,4 @@let paths: Vec<PathBuf> = content.files.iter().map(|file| content.download_dir.join(&file.path))file.pathcomes from Transmission and can be absolute or contain..;Path::jointhen probes and hardlinks outsidedownload_dir. Reject rooted and parent components, and verify the resolved path stays below the download root before selecting a feature.@@ -0,0 +144,4 @@// No expected runtime yet: the movies table carries no TMDB runtime,// so feature selection is by size alone (largest readable video).let selection = match self.prober.select_feature(paths, None).await {select_featureprobes every torrent file serially with a 60-second per-file timeout, but the Reconcile lane cancels its whole action after 25 seconds. A slow or multi-file torrent restarts from the first path every tick and can never import; give imports a compatible budget or persist/bound the work.Fixed in
5856dbc: torrent-declared paths now go throughsafe_join, which refuses absolute paths, drive prefixes and..components before anything is probed or linked. A torrent whose every entry escapes the root hard-fails as "no readable video file". Covered byhostile_torrent_paths_never_leave_the_download_root,a_torrent_of_only_hostile_paths_hard_failsandsafe_join_refuses_escapes_and_keeps_normal_paths.Fixed in
5856dbc: probe results are now cached per path across ticks (ImportAction.probed), so a cancelled tick resumes where it stopped instead of restarting from the first file — each tick makes forward progress as long as a single probe fits the 25 s budget. Entries are dropped once the grab settles. Covered byprobe_results_are_reused_across_calls. The remaining single-probe-over-25s case is a local-disk ffprobe, which stays well under it.@@ -0,0 +120,4 @@let outcome = if let Some(outcome) = cached {outcome} else {let outcome = match self.prober.probe(path.clone()).await {This cache is populated only after
probe()completes. The reconcile timeout cancels a 25–60 second single-file probe before that insertion, so it still restarts forever; use a compatible action/probe budget or retain work that survives cancellation.Fixed in
c6f94aa: each probe now runs as atokio::spawned detached task that writes the cache itself before returning. A tick cancelled by the lane's 25 s budget drops only the await — the probe keeps running (its own 60 s limit intact,kill_on_dropno longer triggered since the task owns the child) and deposits the result for the next tick. Covered bya_cancelled_probe_still_deposits_its_result, which cancelsprobe_allmid-probe and asserts the next call reuses the deposited result with exactly one ffprobe invocation.