Skip to content

Latest commit

 

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

lonecopy

Which of your files exist in only one place?

Duplicate finders tell you what you have twice. lonecopy tells you the opposite: the photos that live only on the external drive from 2019, the documents that never made it to the NAS, the folder your backup job has been silently skipping. It hashes every regular file in each place you register, remembers the results, and reports every file whose content is found in fewer places than you want.

lonecopy window: locations on top, files with too few copies as a tree of location and folder, file list on the right

The window above is python -m lonecopy gui; the same inventory is available from the command line.

  • Locations are directories (a laptop, a mounted drive, a NAS share) or hash lists made by sha256sum, shasum -a 256 or rclone hashsum SHA256, so cloud remotes count without being mounted.
  • Copies are counted across locations. Two identical files on the same disk are one copy. A backup is a second place, not a second file.
  • Rescans are incremental. A file whose size and mtime are unchanged keeps its stored hash; only new and changed files are read again.
  • Safe against unplugged drives. A directory that comes back empty while its inventory is full is refused instead of wiped.
  • Weak matches. Cloud services re-encode photos and editors rewrite metadata, so an exact hash can miss a copy that a human would accept. The report flags lone files that have a same-name (and same-size) file in another location, as a hint rather than a verdict.

Standard library only, Python 3.10+, one SQLite file. No pip install: clone and run as a module.

Five-minute example

python -m lonecopy add laptop ~/Pictures
python -m lonecopy add usb-2019 "/Volumes/USB 2019"
rclone hashsum SHA256 gdrive:Photos > gdrive.sha256
python -m lonecopy add gdrive --hashfile gdrive.sha256
python -m lonecopy report
lonecopy: 1243 files with fewer than 2 copies  (inventory ~/.local/share/lonecopy/inventory.db: 3 locations, 48210 files, 312.4 GB)

laptop  (dir, /Users/me/Pictures)
  1243 files only here, 8.7 GB   [similar file elsewhere: 12 by name-size]
    2019/Sommer                                         801 files     5.1 GB
    2023/Handy                                          402 files     3.4 GB
    by type: .jpg 1104, .heic 98, .mov 41

usb-2019  (dir, /Volumes/USB 2019)
  0 files only here, 0 B

gdrive  (hashfile, /Users/me/gdrive.sha256)
  17 files only here, 0 B, 17 with unknown size

add scans right away. Later, after copying things around:

python -m lonecopy scan            # all locations, only changed files are hashed
python -m lonecopy report --list   # every affected file, tab separated
python -m lonecopy report --json   # for scripts

Desktop window

python -m lonecopy gui

The same inventory, as a window: add folders and hash lists with a file dialog, rescan in the background with a progress line, browse the result as a tree of location and folder, and double-click a file to show it in Finder, Explorer or your Linux file manager. The report options (wanted copies, similar-by, empty files) sit in the toolbar; File > Export writes the JSON report or the tab separated file list.

The window uses tkinter, which is part of Python on Windows, on the python.org builds for macOS, and on most Linux distributions. Where it is missing: brew install python-tk@3.14 (Homebrew), apt install python3-tk (Debian, Ubuntu), or re-run the Windows installer with tcl/tk enabled. The command line works without it.

Commands

Command What it does
add NAME PATH register a directory and scan it (--no-scan to only register)
add NAME --hashfile FILE [--strip-prefix P] register a hash list; P is removed from the front of every path
scan [NAME ...] update one, several or all locations; --ignore GLOB (repeatable), --workers N, --allow-empty
report --min-copies N (default 2), --location NAME, --list, --json, --depth D, --top T, --weak name-size|name|none, --include-empty, --exit-code
locations registered locations with file counts and last scan time
gui open the desktop window
remove NAME forget a location and its inventory

The inventory lives at $LONECOPY_DB, else ~/.local/share/lonecopy/inventory.db; --db PATH overrides both.

What counts, what does not

  • Only regular files. Symlinks are never followed, sockets and devices are skipped.
  • Junk that every copy carries is ignored by default: .DS_Store, Thumbs.db, desktop.ini, AppleDouble ._* files, Spotlight and Trash folders. --no-default-ignore turns that off.
  • Zero-byte files are left out of the report unless --include-empty is given; their hash is identical everywhere and says nothing.
  • Hash lists carry no sizes. Their files count as copies, but their bytes show as unknown, and a weak match against them uses the name only.
  • File names are compared NFC-normalised, so a name written by an old HFS+ volume matches the same name on APFS or ext4.

Cron

python -m lonecopy scan && python -m lonecopy report --exit-code >/dev/null || \
  python -m lonecopy report --top 5 | mail -s "lonecopy: files with one copy" me@example.org

Development

uv venv .venv && uv pip install --python .venv/bin/python pytest
.venv/bin/python -m pytest -q

Not yet

  • A second equality tier for re-encoded photos based on EXIF capture time and camera model, so a Google Photos copy of a JPEG counts as a copy.
  • Reading rclone lsjson --hash output directly, which carries sizes.
  • Hashing files inside archives.

Support

lonecopy is developed and tested on a single laptop. If it saves you time, you can support the work:

GitHub Sponsors PayPal

License

MIT

About

Which of your files exist in only one place? Inverse duplicate finder with SQLite inventory, hash list import and a tkinter window.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages