fix(paths): harden imports, moves, and root relocation#717
fix(paths): harden imports, moves, and root relocation#717therobbiedavis wants to merge 120 commits into
Conversation
Preserve download-client reported path whitespace for torrent source/content paths while keeping destination validation separate. Add user-provided library destination validation for add, move, and root-folder workflows, allowing filesystem roots for root folders while rejecting parent traversal for concrete destinations.
Allow root-folder configuration to accept filesystem roots, including Windows current-drive roots, while keeping concrete destination paths strict against root-only and parent-traversal targets. Add cross-platform root-folder validation coverage and standardize the new torrent path mapper test header.
There was a problem hiding this comment.
Pull request overview
This PR fixes download/import path handling so client-reported torrent paths preserve meaningful leading/trailing whitespace (important on Unix-like filesystems), while adding stricter, OS-aware validation for Listenarr-owned destination paths (root folders, add-to-library, and move operations).
Changes:
- Preserve whitespace in torrent client path mapping and remote path translation by avoiding whitespace-trimming/whitespace-only rejection in key path plumbing.
- Add OS-aware normalization/validation for user-provided destination directories, including explicit support for filesystem roots in root-folder configuration and blocking parent traversal for concrete destinations.
- Add regression tests covering whitespace-bearing torrent folders (Transmission/qBittorrent), root-folder root paths, and invalid destination rejection in library add/move workflows.
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Mocks/Api/TransmissionApiMock.cs | Adds a canned Transmission torrent response with whitespace-bearing folder names for regression coverage. |
| tests/Features/Infrastructure/DownloadClients/Common/TorrentClientPathMapperTests.cs | New tests asserting whitespace preservation and rooted-child handling for torrent file mapping. |
| tests/Features/Infrastructure/DownloadClients/Common/DownloadClientAdapterTests.cs | Adds Transmission adapter regression asserting whitespace-bearing ContentPath/SourceFiles are preserved. |
| tests/Features/Domain/Utils/FileUtilsTests.cs | Adds tests for whitespace-preserving combining/normalization and new destination-path validation rules. |
| tests/Features/Application/Downloads/Common/DownloadClientGatewayTests.cs | Adds tests ensuring remote path mapping and directory expansion keep whitespace-bearing paths intact. |
| tests/Features/Application/Audiobooks/RootFolders/RootFolderServiceTests.cs | Adds coverage for allowing filesystem roots and validating/normalizing root folder paths. |
| tests/Features/Api/Features/Library/LibraryController_MoveTests.cs | Adds API test ensuring invalid move destinations are rejected with 400. |
| tests/Features/Api/Features/Library/LibraryController_AddToLibraryTests.cs | Updates custom path test to be OS-safe and adds parent-traversal/invalid-path rejection tests. |
| listenarr.infrastructure/DownloadClients/Transmission/TransmissionImportPathResolver.cs | Stops treating whitespace-bearing strings as “empty” when building content/source paths. |
| listenarr.infrastructure/DownloadClients/Qbittorrent/QbittorrentImportPathResolver.cs | Same as above for qBittorrent source file translation. |
| listenarr.infrastructure/DownloadClients/Common/TorrentClientPathMapper.cs | Reworks torrent path mapping to avoid trimming path-segment whitespace and better handle rooted-looking child paths. |
| listenarr.infrastructure/Configuration/Paths/RemotePathMappingService.cs | Adjusts empty-path detection to avoid rejecting whitespace-bearing remote paths. |
| listenarr.domain/Common/FileUtils.UserProvidedPaths.cs | New OS-aware user-provided directory path validation/normalization helper with root/traversal options. |
| listenarr.domain/Common/FileUtils.PathCombining.cs | Documents CombineWithOptionalBase’s whitespace-preserving intent and relaxes base-path emptiness check. |
| listenarr.application/Downloads/Common/DownloadClientGateway.cs | Preserves whitespace-bearing paths during mapping/translation and directory-expansion flows. |
| listenarr.application/Audiobooks/RootFolders/RootFolderService.cs | Root folder create/update now uses OS-aware normalization and allows filesystem root paths. |
| listenarr.application/Audiobooks/Contracts/ILibraryAddService.cs | Extends result contract to surface validation failures cleanly to API workflows. |
| listenarr.application/Audiobooks/Catalog/LibraryAddService.cs | Validates custom/generated destination paths with OS-aware normalization and blocks parent traversal. |
| listenarr.api/Features/Library/LibraryMoveWorkflow.cs | Validates and normalizes move destinations (blocking traversal) instead of silently normalizing invalid targets. |
| listenarr.api/Features/Library/LibraryAddWorkflow.cs | Returns 400 for validation failures and validates custom destination paths via OS-aware normalization. |
Make root-folder operations semantics-aware by threading FileSystemPathSemantics through root path checks, repository migrations, deletion safeguards, and unmatched-file filtering, and block new roots that overlap active relocation boundaries. Improve relocation/move reliability with atomic-rename recovery markers, stricter cleanup reconciliation (including ambiguous quarantine detection), resilient heartbeat/broadcast error handling, and retry/reconcile support for superseded move jobs. Remove the temporary frontend debug test placeholder.
Adds lease-generation fencing for move jobs so only the active claimant can heartbeat, update status/phase, and persist move-manifest cleanup state. Move processing now cancels when a lease is lost, and persistence now increments/stores LeaseGeneration with a migration. Path handling was also made semantics-driven across backend and frontend: remote/client path mapping, scan/planning, metadata parsing, and destination validation now honor explicit syntax/case-sensitivity instead of host defaults or path-shape guesses. Tests were updated to cover lease loss behavior, cross-platform path rules, and new architecture boundaries.
Refactor UpdateJobStatusAsync to isolate persistence, relocation, and broadcast into separate try/catch blocks so persistence failures propagate without broadcasting. Add specific catch blocks in MoveBackgroundService and MoveJobProcessor for MoveLeaseLostException and PersistenceException to prevent lease-lost scenarios from marking jobs as Failed. Update root folder change toast messages for clarity. Add tests covering persistence failure propagation, post-commit relocation failures, and lease-loss handling.
Diagnosis: LibraryMoveWorkflow compares user-library source and destination paths, not internal system paths. The relevant semantics are the destination root/output path boundary semantics already resolved from configured root folders or output path. This changes no public API surface; LibraryMoveWorkflow is DI-constructed and callers continue through LibraryController.
|
Temporary verbose Windows test diagnostic superseded by compact output. |
Temporary Windows test diagnosticsExit code: |
Temporary Windows test diagnosticsExit code: |
Temporary Windows test diagnosticsExit code: |
Temporary Windows test diagnosticsExit code: |
|
Bit awkward to raise this mid-PR — apologies if it's noise while you're heads-down. But I ran a reproduction harness against this branch and it turned up something directly relevant to what you're hardening, so here seemed better than a separate issue.
Reproduced on this branch with Listenarr's own default layout Both end up with the author folder as their BasePath — which matters more here, since BasePath now feeds your move orchestration: a book whose BasePath is its author's shelf is a book whose move operates on the whole shelf. The one-line fix (drop the author clause; require the title in the filename or folder) plus tests are up as #766, but it belongs on this branch since you're rewriting that file. It does mean layouts carrying neither title nor author in the path go from wrongly-matched to unmatched — the right direction, and what a tag pass would then claim. The harness runs against a Linux container — which is where a large share of users will probably run Listenarr, and where this branch's cross-platform path handling lands in practice — so it exercises the deployment target rather than a dev box. Happy to run whatever path shapes would help and report back, which is zero setup on your end. Or run it yourself: it's public (https://github.com/m4bard/listenarr-testdata), with a build-and-run-against-a-branch recipe in the README (Linux/macOS or WSL2). There's a small backlog of harness improvements tracked there too, if any of this is useful. |
Summary
This PR hardens Listenarr's filesystem path handling across download imports, manual/library destinations, durable audiobook moves, root-folder relocation, metadata-only path repair, scanning, and frontend destination editing.
The main rule this branch enforces is:
It also replaces fragile BasePath pre-save behavior with durable move orchestration, adds root-folder relocation workflows, restores EF migration discoverability/schema coverage from PR #727, and adds guardrails so future path/migration regressions fail tests instead of silently shipping.
Added
PUT /library/{id}compatibility path.Changed
BasePath, so metadata is updated only after move completion or through explicit metadata-only rewrite paths.PUT /library/{id}BasePath changes through the same destination rewrite service and prevents stale full-object payloads from undoing rewritten path fields.basePath,imageUrl, or file-path fields.NeedsAttentionand continuing recovery.Fixed
BasePathis unavailable.ProcessingvsRunning/NeedsAttention/Superseded) and modal-local subscriptions being torn down before background updates arrive.AddMoveJobSourcePathdesigner, adding the missingProcessExecutionLogsmigration, regenerating durable move migrations via EF tooling, and adding schema coverage.Removed / replaced
Testing
Validated locally on the final branch state with:
cd fe && npx vitest run EditAudiobookModal.moveOptions.spec.ts moveJobs.store.spec.ts --reporter=dot --silent=truecd fe && npm run test:unitcd fe && npm run builddotnet test tests/Listenarr.Tests.csproj --no-restore --verbosity minimal --filter "FullyQualifiedName~EfMoveQueuePersistenceTests|FullyQualifiedName~MoveQueueServiceTests|FullyQualifiedName~LibraryController_MoveTests" -p:OutDir=C:/Users/local_oqoz17v/AppData/Local/Temp/listenarr-test-out/dotnet test tests/Listenarr.Tests.csproj --no-build --verbosity minimal --filter "FullyQualifiedName~BackendArchitectureTests|FullyQualifiedName~HostedServicesRegistrationTests.BackgroundWorkerOwnership_DocumentsEveryHostedService"cd fe && npx prettier --check src/__tests__/EditAudiobookModal.moveOptions.spec.ts src/__tests__/moveJobs.store.spec.ts src/components/domain/audiobook/EditAudiobookModal.vue src/stores/moveJobs.tsgit diff --check origin/canary...HEADAlso previously validated the broader backend suite during review; the only temp-output failures were repo-root fixture/doc lookup tests, which passed when rerun from the normal repository output.
Issues and related PRs
Notes
dotnet ef migrations add; do not hand-author or hand-split migration files.20260702200000./booksor/downloads.