Find Duplicate Files Safely at Scale
HarveySoftware Engineer · Technical ScreenLast updated
Design and implement a robust file duplicate finder that handles filesystem cycles, minimizes disk I/O using multi-stage filtering, and avoids loading entire large files into memory.
Task
Implement a duplicate file detector that scans a given root directory and groups files that have identical byte content. Your solution must be safe against filesystem cycles (e.g., circular symlinks), avoid loading entire files into memory, and minimize unnecessary disk reads when processing large volumes of data.
Requirements
- Cycle & Link Safety: Safely handle symbolic links and hard links. Do not enter infinite loops due to directory symlink cycles. If two distinct paths point to the exact same underlying file (same
(device, inode)), consider whether they should be reported as duplicates or treated as identical references. - Multi-Stage Filtering: Reading full file contents is expensive. Optimize duplicate detection using progressive checks:
- Group files by size first (files with unique sizes cannot be duplicates).
- For files with matching sizes, compute a partial hash (e.g., first 4 KB).
- For files with matching partial hashes, compute a full content hash using chunked streaming.
- Perform a byte-by-byte verification if cryptographic hash collisions must be strictly ruled out.
- Bounded Memory: Large files (e.g., multi-gigabyte video or archive files) must be read in configurable chunk buffers rather than read entirely into RAM.
Interface
from typing import List, Set
import os
class DuplicateFileFinder:
def __init__(self, chunk_size: int = 64 * 1024):
"""
chunk_size: number of bytes to read per buffer chunk.
"""
self.chunk_size = chunk_size
def find_duplicates(self, root_dir: str) -> List[List[str]]:
"""
Scans `root_dir` recursively and returns a list of duplicate groups.
Each group contains two or more file paths with identical content.
"""
pass
Example
Given directory structure:
/data/
├── a.txt (10 bytes, content: "helloworld")
├── b.txt (10 bytes, content: "helloworld")
├── c.txt (10 bytes, content: "foobar1234")
├── sub/
│ ├── d.txt (10 bytes, content: "helloworld")
│ └── symlink_to_root -> /data
Output:
[
["/data/a.txt", "/data/b.txt", "/data/sub/d.txt"]
]
Explanation: c.txt has the same size but different content. The symlink symlink_to_root points back to /data and is safely skipped without an infinite recursion.
Constraints & Edge Cases
- Empty files (
0bytes) should either be grouped together or ignored based on a configurable flag. - Unreadable files (e.g., permission denied) should be skipped gracefully without aborting the traversal.
- File sizes can reach several gigabytes; directory trees can contain millions of files.
Hints
Hint 1
Track visited directory identities using `(stat.st_dev, stat.st_ino)` rather than string paths to avoid infinite loops caused by symlinks.
Hint 2
Avoid computing full file hashes upfront; group by size first, then hash only a small initial chunk (e.g., 4 KB) to eliminate most false matches before streaming the rest.
Hint 3
Ensure chunked streaming is used for all hashing operations so memory consumption remains `O(chunk_size)` regardless of file size.
Follow-up questions
- How would you handle files modified while the scanner is running, and how would you make the process resumable after a crash?
- If the filesystem contains millions of files across network storage (e.g., NFS), how would you parallelize the scan without overwhelming network I/O?
- Under what conditions would you prefer a direct byte-by-byte comparison between candidate pairs over computing SHA-256 hashes?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.