fix(importer): find qBittorrent-compatible client files under content_path when names omit the root folder (#2878) - #2882
Conversation
…_path when names omit the root folder (#2878) Real qBittorrent names a multi-file torrent's files relative to save_path, so each name carries the root folder and save_path + name is the file on disk. rdt-client, and other clients that emulate the qBittorrent API, name them relative to content_path instead, so the same join looked one folder too high, every file was reported missing, and automatic import failed while Manual Import of the same folder worked. save_path + name stays the primary join. When that file is not on this host after the path remap, the resolver tries content_path + name, only when content_path is a directory distinct from save_path and the name does not already start with the content folder's base name (qBittorrent's own shape, where that join would double the root). The candidate is used only if it exists; otherwise the primary path is kept so the missing file is still reported as before. Single-file torrents are untouched because their content_path is the file itself. A DEBUG line names the fallback path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9 Signed-off-by: vavallee <vavallee@protonmail.com>
There was a problem hiding this comment.
Clean fix for a real bug with a well-scoped diff and solid test coverage.
Logic — the three guards in resolveTorrentFilesWithContentFallback are correct:
clientContentPath != ""keeps Transmission / Deluge / rTorrent paths unchanged (they callresolveTorrentFileswhich passes"")!pathOnHost(binderyPath)preserves the save-path join whenever the file is present, so real qBittorrent is unaffectedfirstPathSegment(name) != rootprevents the double-root case; confirmed by the mutation check in the PR description
The new fallback path (filepath.Join(dir, name)) inherits the same filepath.IsAbs / hasDotDotSegment guards already applied to name before it is joined, so the security posture is unchanged.
One FYI (non-blocking): contentDirForFallback (scanner_poll.go:1323) checks the content directory with os.Stat (follows symlinks), while pathOnHost (scanner_poll.go:1337) uses os.Lstat, consistent with importableSourceFile. The inconsistency is benign — a symlink-to-directory enables the fallback path but pathOnHost/filterImportableFiles will still reject a symlink file correctly — but it is slightly surprising. A comment on contentDirForFallback explaining the deliberate Stat vs Lstat split (analogous to the one on importableSourceFile at line 259–265) would help future readers. No change required.
Tests — TestQbittorrentFilesFor_ContentPathFallback covers the real qBittorrent shape, the rdt-client shape, single-file torrents, genuinely missing files, missing content folder, path remap, no-double-root guard, and save-path-wins. The three mutation checks described in the PR body map directly to named subtests. Good.
— 🤖 Bindery triage bot (automated). Reply to correct me; a human will see it.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Closes #2878
Summary
Automatic import from rdt-client (qBittorrent API) found no files even though Manual Import of the same folder worked.
Bindery builds each file path as
save_path + namefrom/api/v2/torrents/files. Real qBittorrent puts the torrent's root folder in every name of a multi-file torrent (Release/book.m4b), so that join is right. rdt-client lists names relative tocontent_pathinstead (book.m4b), so Bindery looked one folder too high (/downloads/bindery/Example Book.m4b) andfilterImportableFilesdropped everything.What changed, in
internal/importer/scanner_poll.go:save_path + namestays the primary join, so real qBittorrent is unchanged (this is the Bug Report: Single-file torrent import scans entire download directory #903 / a23fd8d reasoning in the comment onqbittorrentFilesFor, which still holds).content_path + namewith the same remap, and uses it only if that file exists. It only does this when:content_pathis set and is not the same assave_pathcontent_pathis a directory (single-file torrents, wherecontent_pathis the file, are left alone)content_pathwould double the root folder.resolveTorrentFilesbehaviour.Known limit, left on purpose: if an unrelated file with the same name sits loose in the shared save path, the primary join finds it first. Preferring
content_pathin that case would mean second guessing real qBittorrent, which this change is careful not to do.How it was verified
New table test
TestQbittorrentFilesFor_ContentPathFallback(internal/importer/scanner_qbit_content_path_test.go) uses temp dirs with real files and a fake qBittorrent/torrents/filesendpoint. It covers: real qBittorrent multi-file, rdt-client multi-file (with the trailing slash oncontent_pathfrom the report), single file, a file that is genuinely missing, a missing content folder, the/data/downloads:<local>remap with both name shapes, the no double root guard, and the case where both paths exist andsave_pathwins.origin/main'sscanner_poll.go, the rdt-client and remap subtests fail and report exactly what the issue shows: got.../bindery/Example Book.m4b, want.../bindery/Example Book - Example Series, Book 2 - Example Author/Example Book.m4b, importable files 0.go vet):root-led_name_missing_under_save_path_never_doubles_the_root_folderfailssave_path_join_preferred_when_both_candidates_existfailsgo build ./... && go vet ./..., plusGOOS=windows go build ./...go test ./cmd/... ./internal/...exit 0golangci-lint run ./internal/importer/...: 0 issues🤖 Generated with Claude Code
https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9