fix(filesystem): say why a source file could not be pinned - #930
Open
m4bard wants to merge 1 commit into
Open
Conversation
CheckAsync catches seven exception types and returned one sentence naming none of them, with the exception bound and never used and no logger call anywhere in the file. A locked file, a permissions problem and a path reached through a symlink were indistinguishable afterwards. That gate refuses the import before any destination is created and before FileMover attempts anything, so nothing further down produces a better message. DownloadImportService logs only this reason string, which is why Listenarrs#890 reports that the logs contain nothing explaining the failure. Now logs the exception with its NativeErrorCode, which a Win32Exception keeps out of Message when a custom message is supplied, and names the offending segment when the cause is a symlinked ancestor. The refusal itself is unchanged, and CheckPublicationSource_LinkedAncestor_ReturnsUnsupported still passes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YEVQ7qDJLk5196MFeggWuA
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.
Toward #890, and the same shape as #916.
FileMover.CheckAsyncdecides whether a downloaded file can be published. It runs before any destination is created and beforeFileMoverattempts anything, so when it says no the import stops there. Its final catch (listenarr.infrastructure/FileSystem/FileMover.SourceCapability.cs:104-112) folds seven exception types into one fixed sentence:exceptionis bound and never read, and there is no logger call anywhere in that file.Why nothing downstream recovers it
DownloadImportService.ResolvePublishableSourceProofAsynclogs that reason at Warning and returns null (DownloadImportService.DirectoryOwnership.cs:277). The null becomes anImportResult.ImportFailurereading "Unable to perform {action} on {source} to {destination}", the job records "Unable to import at least one file for the job (see the log entries)", and the download gets "See the log of job {id} for more information". All three point at text that never contains a cause.That sentence is therefore the whole of what an operator has. #890 puts it plainly: "In logs there is nothing that could tell what is wrong."
The neighbours suggest an oversight rather than a policy
The catch immediately above, at
:100-103, returnsexception.Message. The sibling catch over almost the same exception family inFileMover.PathSafety.cs:82-92logs the exception before returning. Both carry the cause; this one is the odd case out.What changes
The catch logs the exception and puts its type and message into the reason.
Two details that turned out to matter. A
Win32Exceptionconstructed with a custom message keeps the errno inNativeErrorCodeand out ofMessage, so the number is logged separately or it is lost. And when the cause is a symlinked ancestor the reason now names the segment, because the raw failure is anENOTDIRfromopenatand that gives an operator nothing to act on.The refusal itself is unchanged.
CheckPublicationSource_LinkedAncestor_ReturnsUnsupportedstill passes, along with the rest of the suite.How I know it is the right message
Running this build against an install whose imports had been failing for days produced, on the first attempt:
which was enough to find the cause in minutes. Before it, the same failure had been mistaken for a
HardlinkCopyregression for a week.There is a separate question about whether that particular cause should be refused at all. That is #929, not this pull request. This one is worth having either way: whatever is decided there, the other six exception types still need to say what they were.
Tests
Two, both
[DirectoryLinkFact]asNativeTestWorkflowContractTestsrequires. They assert that the refusal still happens and only that the reason is actionable. Reverting the change fails one and leaves the other green.Worked through with Claude Code at my direction. The claims above were checked by running them rather than by reading, and I reviewed this before posting.