From 6e93ca6c39168c31d00242562c621e6e0a4d4069 Mon Sep 17 00:00:00 2001 From: poprhythm Date: Wed, 9 Sep 2026 03:16:05 +0000 Subject: [PATCH] Rewrite qbt-relink.sh with a real safety model, re-enable it Root cause of the data-loss incident: repeated manual re-triggering (stop/setLocation/recheck called multiple times across separate debugging invocations) raced against qBittorrent's own automatic incomplete-file management. The 7 clean successes earlier all completed in one uninterrupted pass; the 2 that failed were the ones manually re-triggered while investigating. Redesign: the whole operation is now one blocking pass (stop -> relocate -> rename -> recheck -> poll to completion -> verify -> report) with a hard rule never to call recheck/stop/setLocation against the same hash a second time while a previous run might still be settling. Adds: - A pre-flight filesystem manifest (name+size) of the destination, and a post-recheck comparison against it - the real ground-truth safety net, independent of trusting qBittorrent's self-reported state. - Idempotency: skips already-correctly-relinked torrents rather than re-touching them. Found and fixed a real gap here during testing - the first version trusted qBittorrent's per-file "progress" alone, which can be stale (cached from before a move) and produced a false "already good" on a torrent whose recheck had never actually run at the new location. Now also requires the tracked filenames to match the destination manifest. - setLocation/setDownloadPath verification now polls briefly instead of checking once immediately - both are asynchronous and a single immediate check can read stale data (this exact bug false-failed a real run during testing). - Fixed manifest generation to use printf instead of `stat -c`'s own \t escape handling, which silently emitted a literal backslash-t instead of a real tab and broke `cut -f1` pairing. Verified end-to-end against a real torrent (Babylon 5 S04, 69GB): clean single-pass recheck, post-check confirmed all 22 files intact. Claude-Session: https://claude.ai/code/session_01HZQK6jHmdTpFjFZM8FUnqA --- qbt-relink.sh | 251 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 199 insertions(+), 52 deletions(-) diff --git a/qbt-relink.sh b/qbt-relink.sh index 3a88ce0..1f20013 100755 --- a/qbt-relink.sh +++ b/qbt-relink.sh @@ -23,11 +23,37 @@ # especially for anything with specials/extras where a torrent's files span # more than one destination folder (this script only handles one folder; # see the sonarr skill doc for that case). +# +# SAFETY MODEL (rewritten after a real data-loss incident - see the sonarr +# skill doc, "Incident 3"): +# +# - The whole operation runs as ONE blocking pass: stop -> relocate -> rename +# -> recheck -> poll to completion -> verify -> report. It does NOT return +# control with "poll manually and come back" - that pattern is exactly what +# caused the incident, because re-running recheck/stop by hand later, after +# qBittorrent's own state had moved on, raced against qBittorrent's +# automatic incomplete-file management and let it destroy real files. +# NEVER call recheck/stop/setLocation against the same hash a second time +# while a previous invocation might still be settling - wait for this +# script to finish (or clearly fail) first. +# - Before touching qBittorrent at all, it snapshots the destination folder's +# filenames+sizes (the ground truth - Sonarr already put the real files +# there). After the recheck settles, it re-reads the folder and compares. +# Any file that shrank, vanished, or changed size is treated as data loss, +# reported loudly, and the script does NOT attempt any further remediation +# (no retry, no re-trigger) - investigate by hand from a known-bad state +# rather than risk compounding it. +# - If the torrent already looks correctly relinked (content_path matches and +# on-disk files match the expected sizes), the script does nothing further +# - idempotent, so re-running it after a partial failure is safe rather +# than repeating destructive steps. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ENV_FILE="$HOME/.claude-homelab/.env" +POLL_INTERVAL=15 +POLL_TIMEOUT_S=2400 # 40 min - generous for large multi-GB files over NFS if [[ ! -f "$ENV_FILE" ]]; then echo "Error: $ENV_FILE not found (qBittorrent creds live there, not this repo's .credentials)" >&2 @@ -60,10 +86,6 @@ if [[ "$login_status" != "200" ]]; then fi # api_call