Skip to content

Tolerate a file that vanishes during the hash pass - #156

Merged
matt-edmondson merged 2 commits into
mainfrom
fix/vanished-file-during-hashing
Sep 29, 2026
Merged

matt-edmondson merged 2 commits into
mainfrom
fix/vanished-file-during-hashing

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #141

Problem

After the hash pass, two places read file sizes from disk again without guarding against a file that had since disappeared:

  • The DuplicateGroup constructor read new FileInfo(files[0]).Length. It runs inside FindDuplicates, which Scan, DryRun and Deduplicate all use.
  • Stats summed new FileInfo(f).Length over every hashed file.

If a file was removed or renamed during a multi-minute hash pass, an uncaught FileNotFoundException threw away the whole run. The missing file was often unrelated to any duplicate.

Change

  • FindDuplicates now sizes each copy with a tolerant lookup, TryGetSize, which catches IOException (including not-found) and UnauthorizedAccessException.
    • A copy that has vanished is dropped.
    • A group left with fewer than two copies is dropped.
    • DuplicateGroup takes its size as a constructor argument instead of reading the disk itself.
  • Stats uses a new Deduplicator.TotalSize, which counts nothing for a vanished file.
  • DeleteDuplicates is unchanged. It already re-hashes each copy before deleting it and skips any that can't be confirmed.

I didn't take the issue's other suggestion of recording sizes during hashing. The acceptance criteria require grouping to notice a copy that has vanished, which recorded sizes can't tell you. The existence check at grouping time handles both halves and leaves HashFiles and its roughly 20 test call sites unchanged.

Tests

New tests in DeduplicatorTests:

  • A copy deleted after hashing is dropped from its group. The other two are still reported, with the correct size.
  • A two-copy group where either copy vanishes is dropped (covers the old files[0] crash path).
  • TotalSize skips a vanished file.

I checked that these catch the bug by temporarily making the size lookup unguarded, as the old code was: all four test cases failed, and they pass with the fix. Full suite: 59 passed and 2 skipped locally. The two skips are existing permission tests that can't run as root.

🤖 Generated with Claude Code

https://claude.ai/code/session_015LsGhhi4Vq9pNq2fr5T75T


Generated by Claude Code

After hashing, FindDuplicates sized each group with FileInfo.Length on its
first file, and Stats summed FileInfo.Length over every hashed file. A file
removed or renamed during a multi-minute hash pass made either throw an
uncaught FileNotFoundException, losing the whole DryRun, Deduplicate, Scan or
Stats run -- often over an unrelated file.

FindDuplicates now sizes each copy tolerantly, drops a copy that has
vanished, and drops a group left with fewer than two copies. DuplicateGroup
takes its size from that pass instead of reading the disk itself. Stats sums
sizes through the same tolerant lookup, counting nothing for a vanished file.

Fixes #141

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015LsGhhi4Vq9pNq2fr5T75T
Comment thread FileDeduplicator/Deduplicator.cs Fixed
@sonarqubecloud

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit 4b2bcf4 into main Sep 29, 2026
14 checks passed
@matt-edmondson
matt-edmondson deleted the fix/vanished-file-during-hashing branch September 29, 2026 21:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A file deleted or moved during hashing crashes DryRun/Deduplicate/Stats with FileNotFoundException after all the hashing is done

2 participants