fix(files): fall back to link+unlink when the filesystem rejects renameat2(RENAME_NOREPLACE) — same-volume moves fail on NFS - #944
Draft
kevinheneveld wants to merge 1 commit into
Conversation
…meat2(RENAME_NOREPLACE) Live case: a library on NFS 4.2. The Linux NFS client returns EINVAL for any renameat2 flag, so every same-volume file move through the pinned mover — split-collection transfers, transfer-files, organize/rename — failed with "Could not publish a pinned filesystem entry relative to its owned directory" (errno 22) and left files in place, while the database reassignments went through. Directory moves already had a verified-copy fallback for this errno class; single-file publication did not. RenameNoReplaceLinuxCore now tries renameat2(RENAME_NOREPLACE) first and, on EINVAL/ENOSYS/EOPNOTSUPP, emulates it with linkat + unlinkat: linkat refuses an existing destination (EEXIST) so the no-replace contract holds without a check-then-act race, and the destination keeps the same inode so pinned identity proofs still verify. If the filesystem cannot hard-link either, the original unsupported errno is returned so callers' copy fallbacks still engage; if the source name cannot be unlinked, the new link is rolled back. Both the probing (TryRenameRelativeEntryNoReplaceLinux) and throwing (RenameRelativeEntry) paths share the core, so in-place renames are covered too. Native primitives are injected as delegates; 15 unit tests cover the sequencing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012A3PhZjAJkCP4Cvo9gC551
kevinheneveld
added a commit
to kevinheneveld/Listenarr
that referenced
this pull request
Sep 5, 2026
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012A3PhZjAJkCP4Cvo9gC551
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
On a library stored on NFS 4.2, every same-volume single-file move through the pinned mover fails:
The Linux NFS client returns EINVAL for any
renameat2flag, includingRENAME_NOREPLACE. Probed on the host:renameat2(RENAME_NOREPLACE)→ EINVAL, plainrenameat→ ok,linkat→ ok (and EEXIST when the destination exists). Affected user-facing flows: Split Collection transfers, Transfer Files, Organize/rename. The database reassignment succeeded while the file stayed put, e.g. "Transferred 10 file(s) (0 moved on disk)". Directory moves (AudiobookContentMoveService.MarkerlessRename) already classify 22/38/95 as "native rename unsupported" and fall back to a verified copy; single-file publication had no fallback.Fix
PinnedDirectoryCreation.RenameNoReplaceLinuxCore: tryrenameat2(RENAME_NOREPLACE); on EINVAL/ENOSYS/EOPNOTSUPP emulate it withlinkat+unlinkat:linkatrefuses an existing destination with EEXIST, so the no-replace contract holds without a check-then-act race.Both the probing path (
TryRenameRelativeEntryNoReplaceLinux, used byPinnedFileEntry.MoveTo) and the throwing path (RenameRelativeEntry, used byMoveWithinParent/ in-place renames) share the core. Directory renames keep their previous behavior (hard links don't apply; their copy fallback remains).Tests
NoReplaceRenameFallbackTests(15 cases) drive the core with fake primitives: rename supported; EEXIST without fallback; EINVAL/ENOSYS/EOPNOTSUPP → link+unlink sequence; destination-exists refusal via link; hard links unsupported → original errno; unlink failure rolls back the link; vanished source after link = success; errno classification.Verified on the affected NFS host after deploying to a fork build.
🤖 Generated with Claude Code
https://claude.ai/code/session_012A3PhZjAJkCP4Cvo9gC551