From fddd462ff36fd25015474d9d8f1bb81df302ba0c Mon Sep 17 00:00:00 2001 From: poprhythm Date: Tue, 8 Sep 2026 03:01:22 +0000 Subject: [PATCH] Harden qbt-relink.sh after a live incident: unchecked setLocation let a torrent start re-downloading Batch-relinking 9 torrents, one setLocation call silently failed (status ignored) while the script declared success and moved on. Its recheck then ran against the original (now-empty) path, found 0% match, and qBittorrent started re-downloading the whole torrent from scratch into its incomplete-files staging area. No lasting harm (separate path from the real hardlinked copy, cleaned up), but caught only by watching qBittorrent directly, not by anything the script reported. Fixes: every mutating call now goes through an api_call() helper that checks the HTTP status and aborts on failure; the torrent is stopped before any location/rename calls (qBittorrent 5.x renamed pause/resume to stop/start) and left stopped after recheck rather than auto-resuming, so a bad relink can never turn into an active download. setLocation's effect is also verified via a follow-up GET before proceeding to renameFile. Claude-Session: https://claude.ai/code/session_01HZQK6jHmdTpFjFZM8FUnqA --- .claude/skills/sonarr/SKILL.md | 49 +++++++++++++++++++++++++++----- qbt-relink.sh | 52 +++++++++++++++++++++++++++++----- 2 files changed, 87 insertions(+), 14 deletions(-) diff --git a/.claude/skills/sonarr/SKILL.md b/.claude/skills/sonarr/SKILL.md index d2dc244..c409dc7 100644 --- a/.claude/skills/sonarr/SKILL.md +++ b/.claude/skills/sonarr/SKILL.md @@ -63,24 +63,59 @@ bash ~/.claude/plugins/cache/claude-homelab/homelab-core/*/skills/qbittorrent/sc | python3 -c "import sys,json; [print(t['hash'], t['name']) for t in json.load(sys.stdin)]" | grep -i "" ``` -It sets the torrent's location to the season folder, renames each file to -match Sonarr's output (paired by sorted order — always check the printed -preview makes sense), then triggers a recheck. Since it's the same underlying -data (hardlink, same inode), the hash check passes and the torrent goes back -to seeding normally instead of erroring — no re-download. **The recheck reads -the whole file over NFS and is slow** (minutes per multi-GB file) — kick off -several in parallel rather than waiting on each one serially, and poll with: +It **stops the torrent first** (qBittorrent 5.x renamed pause/resume to +stop/start), sets its location to the season folder — verifying the change +actually took effect before continuing, not just trusting a 200 response — +renames each file to match Sonarr's output (paired by sorted order — always +check the printed preview makes sense), then triggers a recheck, and leaves +the torrent **stopped** afterward rather than auto-resuming. Since it's the +same underlying data (hardlink, same inode), the hash check passes and the +torrent is ready to seed normally again once you manually start it — no +re-download needed. **The recheck reads the whole file over NFS and is +slow** (minutes per multi-GB file) — kick off several in parallel rather than +waiting on each one serially, and poll with: ```bash bash ~/.claude/plugins/cache/claude-homelab/homelab-core/*/skills/qbittorrent/scripts/qbit-api.sh info ``` +Once a torrent shows 100% progress / `stoppedUP` (not `stoppedDL` — that +means the recheck found missing or mismatched pieces), start it again from +the WebUI. **Never assume a relink succeeded without checking** — see the +incident below. + For a multi-season show downloaded as separate per-season torrents (e.g. Babylon 5), run `qbt-relink.sh` once per season/torrent, pointing each at its own `Season NN` folder — don't try to relink multiple torrents to one shared show-root folder, since the tool matches file counts 1:1 between the torrent and the destination folder. +### Incident: an unchecked `setLocation` call let a torrent start re-downloading + +The first version of `qbt-relink.sh` fired `setLocation`/`renameFile`/ +`recheck` with `curl -s ... > /dev/null`, discarding the HTTP status. Running +it across 9 torrents in a batch, one `setLocation` call silently failed (the +torrent still hadn't been re-pointed) while the script declared success +anyway. Its recheck then ran against the original path — already empty, +since Sonarr's import had removed the file — found 0% of pieces present, and +qBittorrent started **re-downloading the entire torrent from scratch** into +qBittorrent's default incomplete-files staging path +(`/data/torrents/incomplete/...`), racking up several minutes of real +download traffic before it was caught and stopped. + +No actual harm resulted — the fresh download went to a separate staging path +and never touched the Sonarr-organized hardlinked copy (confirmed by +checking file counts and Plex/disk state directly), and the stray partial +files were deleted — but it could have gone worse (wasted bandwidth on a +private tracker, or worse, if the download target had somehow overlapped +with the real file). Root cause: no HTTP status checking, and the torrent +was never stopped before being touched. Both are now fixed in the script +(the `api_call` helper checks every response; the torrent is stopped before +any location/rename calls and stays stopped after recheck) — but the lesson +generalizes: **when scripting a batch of mutating API calls, verify each one +actually took effect before moving to the next, especially anything that +could make a client start writing data on its own.** + Credentials (`QBITTORRENT_URL`/`USERNAME`/`PASSWORD`) live in `~/.claude-homelab/.env` (the claude-homelab plugin's credential file), not this repo's `.credentials` — `qbt-relink.sh` sources that file directly. diff --git a/qbt-relink.sh b/qbt-relink.sh index ffa4668..8a7db19 100755 --- a/qbt-relink.sh +++ b/qbt-relink.sh @@ -57,6 +57,30 @@ if [[ "$login_status" != "200" ]]; then exit 1 fi +# api_call