Skip to content

Harden path identity, durable moves, scans, and root relocation - #717

Merged
therobbiedavis merged 464 commits into
canaryfrom
bugfix/unix-folder-name-space
Aug 12, 2026
Merged

Harden path identity, durable moves, scans, and root relocation#717
therobbiedavis merged 464 commits into
canaryfrom
bugfix/unix-folder-name-space

Conversation

@therobbiedavis

@therobbiedavis therobbiedavis commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR began with the Unix/macOS download-folder whitespace bug in #528 and expanded into a repair of the filesystem contracts used by imports, audiobook moves, root-folder relocation, scans, file registration, organize/rename, deletion, and path-bearing persistence.

The branch now separates three concepts that were previously conflated:

  • Download-client source paths are external filesystem identities. Legal whitespace, case, separators, and client-reported shape are preserved rather than normalized as user input.
  • Listenarr-owned destinations and destructive filesystem targets are mutation boundaries. They are validated using explicit filesystem syntax and case semantics, physical-generation evidence, pinned/no-follow filesystem primitives, and race-safe revalidation before mutation.
  • Durable recovery evidence is persisted application state. Move jobs, manifests, directory ledgers, ownership rows, file-mutation journals, audiobook deletion intents, scan handoffs, and root-relocation state live in SQLite/application-owned runtime storage. User library paths contain only requested library content and final directories; cross-process lock files live under the Listenarr config runtime directory.

Physical library moves are durable workflows instead of BasePath pre-saves. Jobs persist endpoint identity, tracked-file manifests, lease/retry state, physical-generation proofs, database-backed mutation journals, target-directory state, cleanup authority, and durable scan handoffs so crashes, cancellation, retries, pathname replacement, and stale workers fail closed.

Issues resolved

Closes #528
Closes #598
Closes #765
Closes #438

Related: #766

Platform-specific mutation contract

  • Windows: relative directory creation returns a creation-bound handle, so a newly created directory can acquire durable cleanup ownership after its physical generation is persisted and revalidated.
  • Unix-like systems: directories are created only at the requested final name with mkdirat, then reopened and verified without following links. Because POSIX does not atomically return a handle for the generation created by mkdirat, a newly created Unix directory is treated as retained/unowned rather than being granted destructive cleanup authority solely from the create result.
  • Unix cross-volume Move: a move that would require copy plus exact-generation source deletion is rejected before journal creation or destination publication. Same-volume native rename remains supported. Copy operations remain supported with content verification.

Changes

Added

  • Syntax-aware filesystem path identity and case-sensitivity resolution for Windows, Unix, UNC, configured roots, scans, move endpoints, and stored file identities.
  • Support for Windows filesystems where case sensitivity is enabled per directory instead of assuming all Windows paths are globally case-insensitive.
  • Durable audiobook move orchestration with manifests, active deduplication, lease fencing, retry/attention states, physical-object generation proofs, database-backed mutation journals, restart reconciliation, source-cleanup boundaries, and target-scaffolding state.
  • File registration, rename, copy, hardlink-copy, and move primitives that publish directly to final names without requiring hidden scratch artifacts under user-managed library paths.
  • Durable owner-bound organize/rename recovery: forward and rollback file journals bind to the audiobook and exact audiobook-file row, and startup reconciles them before ordinary file identity recovery.
  • Durable delete-with-files recovery through SQLite AudiobookDeletionIntent state so process death between filesystem cleanup and database deletion cannot orphan files without recovery authority.
  • Root-folder relocation APIs and frontend flow with persisted relocation state, target reservations, retry/reauthorization, metadata-only mode, skipped-item reporting, collision repair, and relocation-history preservation.
  • Metadata-only root-path repair when the original root is unavailable, fenced behind startup filesystem reconciliation and durable relocation state.
  • Durable move-to-scan handoffs with claim leases, attempt-generation fencing, stale-handoff supersession, and native-rename verification.
  • Global move activity/progress reporting in the application activity UI.
  • Shared filesystem mutation and per-audiobook operation coordinators plus an explicit request-cancellation-to-mutation boundary.
  • Native directory/file symbolic-link capability contracts and platform coverage gates.
  • EF migration/schema regression coverage and architecture gates for filesystem-safety contracts.
  • Application-owned cross-process file-move lock storage at ConfigRootPath/runtime/file-move-locks.

Changed

  • Preserves exact download-client source paths through torrent mapping, remote-path translation, queue expansion, and import.
  • Queues physical moves without pre-saving BasePath; metadata changes only after durable completion or through the explicit metadata-only repair path.
  • Reworks FileMover copy/move/hardlink fallbacks to reject aliases and overlaps, verify publication and cleanup, preserve uncertain content, and avoid recursive copy-and-delete success claims.
  • Same-volume native rename verification uses pinned physical-object generation continuity rather than full-file SHA hashing; copy/fallback paths retain content-hash verification.
  • Unix final-name directory creation does not mint new cleanup ownership unless the exact created generation can be proven independently; move scaffolding created this way is persisted as Retained.
  • Unix cross-volume Move fails closed before publication instead of introducing a hidden source-claim namespace or weakening generation fencing.
  • Source cleanup removes only verified manifest-owned files and authorized empty descendants within an explicit cleanup boundary.
  • Reworks scan attribution around existing ownership, stable identifiers, and book boundaries instead of broad author-path matching.
  • Scan processing uses narrow audiobook reads rather than loading unrelated navigation graphs, and foreign-host fallback output-path warnings no longer spam normal scans.
  • Reworks root-folder create/update/delete/reassign around explicit filesystem semantics, active move/relocation boundaries, atomic persistence, and nested-root conflict prevention.
  • Metadata-only relocation recovery now uses durable Pending, Failed, and NeedsAttention states rather than inferring repairability from human-readable error text.
  • Target-generation replacement during metadata recovery clears stale physical authority instead of silently adopting a replacement directory generation.
  • Collision repair only removes the selected audiobook's currently conflicting tracked metadata row; external owners can be surfaced for diagnosis but cannot be deleted through another audiobook's repair workflow.
  • Move, organize/rename, deletion recovery, and import operations all fence metadata repair through the shared filesystem-mutation authority model.
  • Download import replay now reuses a byte-identical existing suffix when it belongs to the same audiobook and continues suffix planning when an identical path is owned by another audiobook, preventing unnecessary (2)/(3) duplicates and duplicate AudiobookFile rows.
  • Mutation-root collection preserves case-distinct canonical roots rather than deduplicating all Windows roots case-insensitively.
  • Common-directory calculation preserves case-distinct path segments by default and honors explicitly supplied Windows/Unix filesystem syntax independently of the host OS.
  • Durable recovery identities preserve exact persisted path/artifact identities rather than using host-wide case folding.
  • Frontend configured-root path joins use the selected filesystem syntax. A legal Unix \\ remains a filename character instead of being rewritten as a separator, including when the Unix root itself contains a literal backslash.
  • Root-folder settings now reports relocation progress and repair attention with collision-aware copy instead of presenting every ownership conflict as a case-only collision.

Fixed

  • Leading/trailing-whitespace download folders on Unix/macOS ([Linux/OSX] Folder names with leading/trailing space #528).
  • Hardlink/copy success, fallback-copy, source-preservation, and native validation gaps tracked by [Testing] Validate hardlink/copy import behavior #598.
  • Same-author scans attributing sibling books and collapsing BasePath to the author folder (Library scan attributes every book by an author to whichever one is scanned, and sets the author folder as its BasePath #765); the fix/regression approach proposed in fix(scan): don't attribute every book by an author to whichever one is scanned #766 was adapted onto this PR.
  • Recursive/overlapping directory-copy hazards, symlink/hardlink alias false-successes, and replacement races.
  • Invalid or traversal-bearing custom destinations silently falling back to generated paths.
  • .. traversal into case-distinct Windows sibling directories.
  • Mutation-boundary escapes caused by assuming Windows path comparisons are always case-insensitive.
  • Case-distinct directory-copy snapshot entries collapsing into the same internal identity.
  • Case-distinct durable recovery identities being merged on case-sensitive filesystems.
  • Root relocation vs standalone move races, stale leases, unsafe retry/requeue state, and incomplete restart recovery.
  • Root path changes being permanently blocked when the configured source root is unavailable even though a safe metadata-only repair is possible.
  • Metadata-only repair being attempted before startup recovery has established a trustworthy filesystem state.
  • Missing relocation targets incorrectly requiring physical target identity before metadata-only repair could continue.
  • Replaced target generations being adopted during recovery instead of invalidating stale physical authority.
  • Malformed/unattributable audiobook paths being claimed by unrelated root relocations.
  • Non-repairable metadata path failures being published as partial relocation state instead of rejecting before publication.
  • Ordinary path ownership collisions and unresolved target-identity conflicts lacking a scoped operator repair workflow.
  • Historical completed import journals without an owner-bound AudiobookFileId incorrectly blocking root collision repair forever.
  • Case-sensitive/case-insensitive identity mismatches across roots, scans, imports, moves, tracked files, and durable recovery state.
  • Empty source author/series/title folders remaining after successful moves when cleanup authority is actually proven.
  • Stale move-scan handoffs retrying obsolete destinations after a newer move.
  • Native-renamed files failing post-move scan verification because no SHA was persisted.
  • EF scan graph-loading warnings and nondeterministic row-limiting query warnings on the move path.
  • EF migration discovery/schema drift from missing or inconsistent generated migration metadata.
  • Organize/rename crashes after filesystem publication but before audiobook path persistence; owner-bound forward and rollback journals now reconcile before ordinary file identity startup work.
  • Delete-with-files crashes between tracked-generation cleanup and audiobook database deletion; SQLite deletion intents preserve recovery authority and prevent orphaned files.
  • Server-resolved Unix move destinations losing legal leading/trailing whitespace in the edit modal.
  • Add/edit library dialogs rewriting legal Unix literal backslashes because separator choice was inferred from string contents instead of explicit path syntax.
  • Rename recovery trusting nullable/reloaded owner fields after the initial owner-binding check; recovery now revalidates the owner binding after the second SQLite read.
  • Fresh Docker/Podman images failing file mutation because .NET LocalApplicationData could resolve empty when $HOME/.local/share did not exist; cross-process move locks now use the Listenarr-owned config runtime directory.

Removed

  • Physical-move BasePath pre-save behavior.
  • Branch-only library-side scratch/retirement namespace logic. It was never released and has no compatibility path.
  • New-operation dependency on library-side temporary or permanent sidecar files.
  • Host-default path-comparison fallbacks from library mutation/identity decisions.
  • Host-wide Windows == case-insensitive assumptions from destructive mutation authorization and durable recovery identity.
  • Recursive copy-and-delete as a trusted directory-move fallback.
  • Modal-owned long-lived move SignalR handling.

Testing and current validation status

Local Windows validation of the final unchanged tree

  • Full backend suite: 2,663 passed, 87 platform/capability skips, 0 failed.
  • Frontend: 557/557 tests passed across 88/88 test files.
  • Focused root-relocation/metadata-repair/import coverage: 159 passed, 4 skipped, 0 failed.
  • Architecture + production composition validation: 45/45 passed.
  • Frontend lint: passed.
  • Frontend type-check: passed.
  • Production frontend build: passed.
  • Release .NET build: 0 warnings, 0 errors.
  • dotnet format --verify-no-changes: passed.
  • dotnet ef migrations has-pending-model-changes: no pending model changes.
  • Repository path-safety validation: passed.
  • git diff --check: passed.

Native GitHub validation

Exact-head Run Tests workflow 31510036348 is green for ac211dba8e4710eaa9be583d035b0dc551d2eebe.

Linux (unit-tests)

  • Native backend suite: 2,676 passed / 2,762 total.
  • Platform/capability skips: 86.
  • Failures: 0.
  • Frontend: 557/557 tests passed across 88/88 test files.
  • Solution build: 0 warnings, 0 errors.
  • Linux API publish: passed.

Windows (backend-tests-windows)

  • Native backend suite: 2,712 passed / 2,759 total.
  • Platform/capability skips: 47.
  • Failures: 0.
  • Backend build: 0 warnings, 0 errors.

All current exact-head checks, including CodeQL and the minor version-label check, are green. There are currently zero unresolved GitHub Code Quality review threads.

External Docker/Podman reproduction

The Docker lock-directory defect reported by @m4bard is addressed by moving cross-process locks to the Listenarr-owned config runtime root. The original unprimed-container reproduction was rerun independently against a descendant containing that fix and passed all expected import modes. The independent rerun also confirmed that the runtime lock hierarchy is created from an initially absent runtime/ directory, lock permissions remain current-user-only, and no lock artifacts are leaked into the library tree.

Review status

The final unchanged tree completed two consecutive independent adversarial review passes with zero confirmed findings.

The review covered the complete authoritative remediation diff, including:

  • composition/DI and production service registration;
  • persistence, EF model state, and migration boundaries;
  • concurrency, cancellation, filesystem mutation coordination, and recovery fencing;
  • path identity, filesystem/security boundaries, and platform-specific behavior;
  • root relocation start/retry/recovery/abandonment and metadata-only repair;
  • audiobook ownership conflict repair and external-owner isolation;
  • download-import replay/idempotency;
  • frontend/backend repair contracts and operator-facing collision semantics;
  • native Windows and Linux validation on the exact pushed commit.

The final source tree remained unchanged after the second clean review pass and exact-head native validation.

@therobbiedavis therobbiedavis added the patch patch version bump - backward compatible bug fixes label Jul 1, 2026
@therobbiedavis
therobbiedavis marked this pull request as ready for review July 1, 2026 21:04
@therobbiedavis
therobbiedavis requested review from a team and a lite review from Copilot July 1, 2026 21:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread listenarr.infrastructure/DownloadClients/Common/TorrentClientPathMapper.cs Outdated
Comment thread tests/Features/Application/Downloads/Common/DownloadClientGatewayTests.cs Outdated
Comment thread tests/Features/Application/Downloads/Common/DownloadClientGatewayTests.cs Outdated
Comment thread fe/src/utils/path.ts Fixed
Comment thread fe/src/utils/path.ts Fixed
Comment thread listenarr.infrastructure/Library/Moving/MoveBackgroundService.cs Fixed
Comment thread listenarr.infrastructure/FileSystem/FileSystemSemanticsResolver.cs Fixed
Comment thread listenarr.infrastructure/Library/Moving/MoveBackgroundService.cs Fixed
@therobbiedavis therobbiedavis changed the title fix(downloads): preserve client path whitespace fix(paths): harden imports and library moves Jul 8, 2026
Comment thread listenarr.infrastructure/FileSystem/FileSystemSemanticsResolver.cs Fixed
@therobbiedavis therobbiedavis changed the title fix(paths): harden imports and library moves fix(paths): harden imports, moves, and root relocation Jul 9, 2026
@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Temporary formatter/compiler diagnostic superseded and cleaned up.

3 similar comments
@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Temporary formatter/compiler diagnostic superseded and cleaned up.

@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Temporary formatter/compiler diagnostic superseded and cleaned up.

@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Temporary formatter/compiler diagnostic superseded and cleaned up.

Comment thread listenarr.infrastructure/FileSystem/FileSystemSafety.cs Fixed
@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Temporary verbose Windows test diagnostic superseded by compact output.

@github-actions

Copy link
Copy Markdown
Contributor

Temporary Windows test diagnostics

Exit code: 1

FAILED: Listenarr.Tests.Features.Application.Audiobooks.RootFolders.RootFolderServiceTests.Delete_ReassignsFilesystemRootChildAudiobookPreservingRelativePath
System.InvalidOperationException : An error was generated for warning 'Microsoft.EntityFrameworkCore.Database.Transaction.TransactionIgnoredWarning': Transactions are not supported by the in-memory store. See https://go.microsoft.com/fwlink/?LinkId=800142 This exception can be suppressed or logged by passing event ID 'InMemoryEventId.TransactionIgnoredWarning' to the 'ConfigureWarnings' method in 'DbContext.OnConfiguring' or 'AddDbContext'.
at Microsoft.EntityFrameworkCore.Diagnostics.EventDefinition.Log[TLoggerCategory](IDiagnosticsLogger`1 logger, Exception exception)
   at Microsoft.EntityFrameworkCore.InMemory.Internal.InMemoryLoggerExtensions.TransactionIgnoredWarning(IDiagnosticsLogger`1 diagnostics)
   at Microsoft.EntityFrameworkCore.InMemory.Storage.Internal.InMemoryTransactionManager.BeginTransactionAsync(CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade.BeginTransactionAsync(CancellationToken cancellationToken)
   at Listenarr.Infrastructure.Persistence.Repositories.EfRootFolderRepository.ReassignAudiobooksAndRemoveAsync(Int32 sourceRootId, Int32 targetRootId, FileSystemPathSemantics sourceSemantics, FileSystemPathSemantics targetSemantics, CancellationToken ct) in D:\a\Listenarr\Listenarr\listenarr.infrastructure\Persistence\Repositories\EfRootFolderRepository.cs:line 190
   at Listenarr.Infrastructure.Persistence.Repositories.EfRootFolderRepository.ReassignAudiobooksAndRemoveAsync(Int32 sourceRootId, Int32 targetRootId, FileSystemPathSemantics sourceSemantics, FileSystemPathSemantics targetSemantics, CancellationToken ct) in D:\a\Listenarr\Listenarr\listenarr.infrastructure\Persistence\Repositories\EfRootFolderRepository.cs:line 251
   at Listenarr.Application.Audiobooks.RootFolders.RootFolderService.DeleteCoreAsync(Int32 id, Nullable`1 reassignRootId) in D:\a\Listenarr\Listenarr\listenarr.application\Audiobooks\RootFolders\RootFolderService.cs:line 115
   at Listenarr.Application.Common.FilesystemMutationCoordinator.ExecuteExclusiveAsync(Func`2 operation, CancellationToken cancellationToken) in D:\a\Listenarr\Listenarr\listenarr.application\Common\FilesystemMutationCoordinator.cs:line 20
   at Listenarr.Tests.Features.Application.Audiobooks.RootFolders.RootFolderServiceTests.Delete_ReassignsFilesystemRootChildAudiobookPreservingRelativePath() in D:\a\Listenarr\Listenarr\tests\Features\Application\Audiobooks\RootFolders\RootFolderServiceTests.cs:line 560
--- End of stack trace from previous location ---

FAILED: Listenarr.Tests.Features.Infrastructure.Library.Moving.AudiobookContentMoveServiceTests.MoveContentsAsync_DirectCopyMarkerWithoutManifest_BlocksRecovery
Assert.Throws() Failure: No exception was thrown
Expected: typeof(Listenarr.Infrastructure.Library.Moving.MoveNeedsAttentionException)
at Listenarr.Tests.Features.Infrastructure.Library.Moving.AudiobookContentMoveServiceTests.MoveContentsAsync_DirectCopyMarkerWithoutManifest_BlocksRecovery() in D:\a\Listenarr\Listenarr\tests\Features\Infrastructure\Library\Moving\AudiobookContentMoveServiceTests.cs:line 247
--- End of stack trace from previous location ---

FAILED: Listenarr.Tests.Features.Infrastructure.Library.Moving.AudiobookContentMoveServiceTests.MoveContentsAsync_TargetContainsOnlySourceSubtree_AllowsMove
Listenarr.Infrastructure.Library.Moving.MoveNeedsAttentionException : Destination contains an unowned directory: nested
at Listenarr.Infrastructure.Library.Moving.AudiobookContentMoveService.ValidateExistingDestinationContents(String source, String destinationRoot, IReadOnlyCollection`1 manifest, Guid jobId, FileSystemPathSemantics targetSemantics) in D:\a\Listenarr\Listenarr\listenarr.infrastructure\Library\Moving\AudiobookContentMoveService.Copy.cs:line 128
   at Listenarr.Infrastructure.Library.Moving.AudiobookContentMoveService.CopySourceContentsAsync(String source, String copyDestination, IReadOnlyList`1 manifest, Guid jobId, FileSystemPathSemantics sourceSemantics, FileSystemPathSemantics targetSemantics, CancellationToken cancellationToken) in D:\a\Listenarr\Listenarr\listenarr.infrastructure\Library\Moving\AudiobookContentMoveService.Copy.cs:line 17
   at Listenarr.Infrastructure.Library.Moving.AudiobookContentMoveService.MoveContentsAsync(AudiobookContentMoveRequest request, CancellationToken cancellationToken) in D:\a\Listenarr\Listenarr\listenarr.infrastructure\Library\Moving\AudiobookContentMoveService.cs:line 169
   at Listenarr.Tests.Features.Infrastructure.Library.Moving.AudiobookContentMoveServiceTests.MoveContentsAsync_TargetContainsOnlySourceSubtree_AllowsMove() in D:\a\Listenarr\Listenarr\tests\Features\Infrastructure\Library\Moving\AudiobookContentMoveServiceTests.cs:line 583
--- End of stack trace from previous location ---

FAILED: Listenarr.Tests.Features.Infrastructure.Persistence.RootFolderReassignmentTransactionTests.ReassignAudiobooksAndRemoveAsync_RewritesAllReferencesAndDeletesRoot
System.InvalidOperationException : An error was generated for warning 'Microsoft.EntityFrameworkCore.Database.Transaction.TransactionIgnoredWarning': Transactions are not supported by the in-memory store. See https://go.microsoft.com/fwlink/?LinkId=800142 This exception can be suppressed or logged by passing event ID 'InMemoryEventId.TransactionIgnoredWarning' to the 'ConfigureWarnings' method in 'DbContext.OnConfiguring' or 'AddDbContext'.
at Microsoft.EntityFrameworkCore.Diagnostics.EventDefinition.Log[TLoggerCategory](IDiagnosticsLogger`1 logger, Exception exception)
   at Microsoft.EntityFrameworkCore.InMemory.Internal.InMemoryLoggerExtensions.TransactionIgnoredWarning(IDiagnosticsLogger`1 diagnostics)
   at Microsoft.EntityFrameworkCore.InMemory.Storage.Internal.InMemoryTransactionManager.BeginTransactionAsync(CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade.BeginTransactionAsync(CancellationToken cancellationToken)
   at Listenarr.Infrastructure.Persistence.Repositories.EfRootFolderRepository.ReassignAudiobooksAndRemoveAsync(Int32 sourceRootId, Int32 targetRootId, FileSystemPathSemantics sourceSemantics, FileSystemPathSemantics targetSemantics, CancellationToken ct) in D:\a\Listenarr\Listenarr\listenarr.infrastructure\Persistence\Repositories\EfRootFolderRepository.cs:line 190
   at Listenarr.Infrastructure.Persistence.Repositories.EfRootFolderRepository.ReassignAudiobooksAndRemoveAsync(Int32 sourceRootId, Int32 targetRootId, FileSystemPathSemantics sourceSemantics, FileSystemPathSemantics targetSemantics, CancellationToken ct) in D:\a\Listenarr\Listenarr\listenarr.infrastructure\Persistence\Repositories\EfRootFolderRepository.cs:line 251
   at Listenarr.Tests.Features.Infrastructure.Persistence.RootFolderReassignmentTransactionTests.ReassignAudiobooksAndRemoveAsync_RewritesAllReferencesAndDeletesRoot() in D:\a\Listenarr\Listenarr\tests\Features\Infrastructure\Persistence\RootFolderReassignmentTransactionTests.cs:line 43
--- End of stack trace from previous location ---

FAILED: Listenarr.Tests.Features.Infrastructure.Persistence.RootFolderReassignmentTransactionTests.ReassignAudiobooksAndRemoveAsync_DeleteConflictRollsBackPathRewrites
Assert.Throws() Failure: Exception type was not an exact match
Expected: typeof(Microsoft.EntityFrameworkCore.DbUpdateException)
Actual:   typeof(System.InvalidOperationException)
---- System.InvalidOperationException : An error was generated for warning 'Microsoft.EntityFrameworkCore.Database.Transaction.TransactionIgnoredWarning': Transactions are not supported by the in-memory store. See https://go.microsoft.com/fwlink/?LinkId=800142 This exception can be suppressed or logged by passing event ID 'InMemoryEventId.TransactionIgnoredWarning' to the 'ConfigureWarnings' method in 'DbContext.OnConfiguring' or 'AddDbContext'.
at Listenarr.Tests.Features.Infrastructure.Persistence.RootFolderReassignmentTransactionTests.ReassignAudiobooksAndRemoveAsync_DeleteConflictRollsBackPathRewrites() in D:\a\Listenarr\Listenarr\tests\Features\Infrastructure\Persistence\RootFolderReassignmentTransactionTests.cs:line 95
--- End of stack trace from previous location ---
----- Inner Stack Trace -----
   at Microsoft.EntityFrameworkCore.Diagnostics.EventDefinition.Log[TLoggerCategory](IDiagnosticsLogger`1 logger, Exception exception)
   at Microsoft.EntityFrameworkCore.InMemory.Internal.InMemoryLoggerExtensions.TransactionIgnoredWarning(IDiagnosticsLogger`1 diagnostics)
   at Microsoft.EntityFrameworkCore.InMemory.Storage.Internal.InMemoryTransactionManager.BeginTransactionAsync(CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade.BeginTransactionAsync(CancellationToken cancellationToken)
   at Listenarr.Infrastructure.Persistence.Repositories.EfRootFolderRepository.ReassignAudiobooksAndRemoveAsync(Int32 sourceRootId, Int32 targetRootId, FileSystemPathSemantics sourceSemantics, FileSystemPathSemantics targetSemantics, CancellationToken ct) in D:\a\Listenarr\Listenarr\listenarr.infrastructure\Persistence\Repositories\EfRootFolderRepository.cs:line 190
   at Listenarr.Infrastructure.Persistence.Repositories.EfRootFolderRepository.ReassignAudiobooksAndRemoveAsync(Int32 sourceRootId, Int32 targetRootId, FileSystemPathSemantics sourceSemantics, FileSystemPathSemantics targetSemantics, CancellationToken ct) in D:\a\Listenarr\Listenarr\listenarr.infrastructure\Persistence\Repositories\EfRootFolderRepository.cs:line 251

@github-actions

Copy link
Copy Markdown
Contributor

Temporary Windows test diagnostics

Exit code: 1

FAILED: Listenarr.Tests.Features.Infrastructure.Library.Moving.AudiobookContentMoveServiceTests.MoveContentsAsync_DirectCopyMarkerWithoutManifest_BlocksRecovery
Assert.Throws() Failure: No exception was thrown
Expected: typeof(Listenarr.Infrastructure.Library.Moving.MoveNeedsAttentionException)
at Listenarr.Tests.Features.Infrastructure.Library.Moving.AudiobookContentMoveServiceTests.MoveContentsAsync_DirectCopyMarkerWithoutManifest_BlocksRecovery() in D:\a\Listenarr\Listenarr\tests\Features\Infrastructure\Library\Moving\AudiobookContentMoveServiceTests.cs:line 247
--- End of stack trace from previous location ---

FAILED: Listenarr.Tests.Features.Infrastructure.Persistence.RootFolderReassignmentTransactionTests.ReassignAudiobooksAndRemoveAsync_RewritesAllReferencesAndDeletesRoot
Assert.Equal() Failure: Strings differ
                                     ↓ (pos 91)
Expected: ···"547ad4a\\root-reassign-target\\Author\\Title"
Actual:   ···"547ad4a\\root-reassign-source\\Author\\Title"
                                     ↑ (pos 91)
at Listenarr.Tests.Features.Infrastructure.Persistence.RootFolderReassignmentTransactionTests.ReassignAudiobooksAndRemoveAsync_RewritesAllReferencesAndDeletesRoot() in D:\a\Listenarr\Listenarr\tests\Features\Infrastructure\Persistence\RootFolderReassignmentTransactionTests.cs:line 53
--- End of stack trace from previous location ---

FAILED: Listenarr.Tests.Features.Infrastructure.Library.Moving.AudiobookContentMoveServiceTests.MoveContentsAsync_TargetContainsOnlySourceSubtree_AllowsMove
Listenarr.Infrastructure.Library.Moving.MoveNeedsAttentionException : Destination contains an unowned directory: nested
at Listenarr.Infrastructure.Library.Moving.AudiobookContentMoveService.ValidateExistingDestinationContents(String source, String destinationRoot, IReadOnlyCollection`1 manifest, Guid jobId, FileSystemPathSemantics targetSemantics) in D:\a\Listenarr\Listenarr\listenarr.infrastructure\Library\Moving\AudiobookContentMoveService.Copy.cs:line 128
   at Listenarr.Infrastructure.Library.Moving.AudiobookContentMoveService.CopySourceContentsAsync(String source, String copyDestination, IReadOnlyList`1 manifest, Guid jobId, FileSystemPathSemantics sourceSemantics, FileSystemPathSemantics targetSemantics, CancellationToken cancellationToken) in D:\a\Listenarr\Listenarr\listenarr.infrastructure\Library\Moving\AudiobookContentMoveService.Copy.cs:line 17
   at Listenarr.Infrastructure.Library.Moving.AudiobookContentMoveService.MoveContentsAsync(AudiobookContentMoveRequest request, CancellationToken cancellationToken) in D:\a\Listenarr\Listenarr\listenarr.infrastructure\Library\Moving\AudiobookContentMoveService.cs:line 169
   at Listenarr.Tests.Features.Infrastructure.Library.Moving.AudiobookContentMoveServiceTests.MoveContentsAsync_TargetContainsOnlySourceSubtree_AllowsMove() in D:\a\Listenarr\Listenarr\tests\Features\Infrastructure\Library\Moving\AudiobookContentMoveServiceTests.cs:line 583
--- End of stack trace from previous location ---

FAILED: Listenarr.Tests.Features.Infrastructure.Persistence.RootFolderReassignmentTransactionTests.ReassignAudiobooksAndRemoveAsync_DeleteConflictRollsBackPathRewrites
Assert.Throws() Failure: No exception was thrown
Expected: typeof(Microsoft.EntityFrameworkCore.DbUpdateException)
at Listenarr.Tests.Features.Infrastructure.Persistence.RootFolderReassignmentTransactionTests.ReassignAudiobooksAndRemoveAsync_DeleteConflictRollsBackPathRewrites() in D:\a\Listenarr\Listenarr\tests\Features\Infrastructure\Persistence\RootFolderReassignmentTransactionTests.cs:line 95
--- End of stack trace from previous location ---

@github-actions

Copy link
Copy Markdown
Contributor

Temporary Windows test diagnostics

Exit code: 1

FAILED: Listenarr.Tests.Features.Infrastructure.Persistence.RootFolderReassignmentTransactionTests.ReassignAudiobooksAndRemoveAsync_RewritesAllReferencesAndDeletesRoot
Assert.Equal() Failure: Strings differ
                                     ↓ (pos 91)
Expected: ···"7e36cc8\\root-reassign-target\\Author\\Title"
Actual:   ···"7e36cc8\\root-reassign-source\\Author\\Title"
                                     ↑ (pos 91)
at Listenarr.Tests.Features.Infrastructure.Persistence.RootFolderReassignmentTransactionTests.ReassignAudiobooksAndRemoveAsync_RewritesAllReferencesAndDeletesRoot() in D:\a\Listenarr\Listenarr\tests\Features\Infrastructure\Persistence\RootFolderReassignmentTransactionTests.cs:line 53
--- End of stack trace from previous location ---

FAILED: Listenarr.Tests.Features.Infrastructure.Library.Moving.AudiobookContentMoveServiceTests.MoveContentsAsync_DirectCopyMarkerWithoutManifest_BlocksRecovery
Assert.Throws() Failure: No exception was thrown
Expected: typeof(Listenarr.Infrastructure.Library.Moving.MoveNeedsAttentionException)
at Listenarr.Tests.Features.Infrastructure.Library.Moving.AudiobookContentMoveServiceTests.MoveContentsAsync_DirectCopyMarkerWithoutManifest_BlocksRecovery() in D:\a\Listenarr\Listenarr\tests\Features\Infrastructure\Library\Moving\AudiobookContentMoveServiceTests.cs:line 247
--- End of stack trace from previous location ---

FAILED: Listenarr.Tests.Features.Infrastructure.Persistence.RootFolderReassignmentTransactionTests.ReassignAudiobooksAndRemoveAsync_DeleteConflictRollsBackPathRewrites
Assert.Throws() Failure: No exception was thrown
Expected: typeof(Microsoft.EntityFrameworkCore.DbUpdateException)
at Listenarr.Tests.Features.Infrastructure.Persistence.RootFolderReassignmentTransactionTests.ReassignAudiobooksAndRemoveAsync_DeleteConflictRollsBackPathRewrites() in D:\a\Listenarr\Listenarr\tests\Features\Infrastructure\Persistence\RootFolderReassignmentTransactionTests.cs:line 95
--- End of stack trace from previous location ---

@github-actions

Copy link
Copy Markdown
Contributor

Temporary Windows test diagnostics

Exit code: 1

FAILED: Listenarr.Tests.Features.Infrastructure.Persistence.RootFolderReassignmentTransactionTests.ReassignAudiobooksAndRemoveAsync_DeleteConflictRollsBackPathRewrites
Assert.Throws() Failure: No exception was thrown
Expected: typeof(Microsoft.EntityFrameworkCore.DbUpdateException)
at Listenarr.Tests.Features.Infrastructure.Persistence.RootFolderReassignmentTransactionTests.ReassignAudiobooksAndRemoveAsync_DeleteConflictRollsBackPathRewrites() in D:\a\Listenarr\Listenarr\tests\Features\Infrastructure\Persistence\RootFolderReassignmentTransactionTests.cs:line 143
--- End of stack trace from previous location ---

FAILED: Listenarr.Tests.Features.Infrastructure.Library.Moving.AudiobookContentMoveServiceTests.MoveContentsAsync_DirectCopyMarkerWithoutManifest_BlocksRecovery
Assert.Throws() Failure: No exception was thrown
Expected: typeof(Listenarr.Infrastructure.Library.Moving.MoveNeedsAttentionException)
at Listenarr.Tests.Features.Infrastructure.Library.Moving.AudiobookContentMoveServiceTests.MoveContentsAsync_DirectCopyMarkerWithoutManifest_BlocksRecovery() in D:\a\Listenarr\Listenarr\tests\Features\Infrastructure\Library\Moving\AudiobookContentMoveServiceTests.cs:line 247
--- End of stack trace from previous location ---

@m4bard

m4bard commented Aug 7, 2026

Copy link
Copy Markdown

I ran the same repro against 471198c3, which has d7c217b8 as an ancestor. Unprimed container, no --prime-lock-dir, nothing created by hand:

./tools/validate_import_destination.sh --image <471198c3 build>

All three modes now run to completion, where every one of them previously failed at the gate:

new          pass
preexisting  pass
unpatched    reproduced  (control: SHOULD reproduce)

I think that is the confirmation you were after, though you are better placed than me to say whether it covers what you wanted checked.

The control is the case I expect to fail, so it still failing is what suggests to me the check can detect a broken destination rather than having quietly stopped testing anything.

Three of your claims looked observable from the host side, so I checked those rather than taking them on trust. All three seem to hold, as far as I can tell.

The lock hierarchy was completely absent at the start. My config directory is created fresh for each run and holds only config.json and a provisioned ffprobe, with no runtime/ at all. After the first import it contains:

config/runtime/file-move-locks/stripe-1604.lock
config/runtime/file-move-locks/stripe-2407.lock

Permissions came out drwx------ on the directory and -rw------- on the lock files, which I read as the current-user restriction surviving the move.

I could not find anything leaked into the library tree either. Counting every file under both root folders that is not generated audio or my own manifest gives zero.

Worth saying what that does not cover. I did not test the symlinked-ancestor rejection or the unavailable-lock-root fail-closed path, since I cannot stage either from outside the container without contriving the filesystem into a state that would not tell you much, and your regressions look like better coverage there anyway. This is also one container runtime on one host, so it says nothing about the Windows path.

Thanks for turning it around so quickly.

Comment thread listenarr.api/Features/Library/LibraryMoveWorkflow.Paths.cs
Comment thread listenarr.application/Downloads/Import/DownloadImportService.cs
@therobbiedavis therobbiedavis changed the title feat(filesystem): harden path identity, durable moves, scans, and root relocation Harden path identity, durable moves, scans, and root relocation Aug 12, 2026
@therobbiedavis
therobbiedavis merged commit e6300c0 into canary Aug 12, 2026
10 checks passed
@therobbiedavis
therobbiedavis deleted the bugfix/unix-folder-name-space branch August 12, 2026 14:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

minor minor version bump - add functionality in a backward compatible manner

Projects

None yet

5 participants