Safety model
photosift is built so that a bad plan, a crash or a change of heart costs nothing.
Rules
- Nothing moves without a plan.
scan,dupes,statsandplanonly read your files (and write the cache and reports). Onlyapplyandfix-dates --executemodify photos. - Dry run by default.
apply,undoandfix-datesprint what they would do and need--executeto act. - Reviewable plan.
plan.jsonlists every file with its action, destination, reason, size and SHA-256. Edit or delete entries before applying. - Copy by default.
--moveis opt-in. - Verify before removing. Each file is copied to a hidden temp file in the destination directory while hashing the source stream, flushed with
fsync, re-read and compared, then renamed into place withos.replace. Only after that is the journal line written and, in move mode, the source removed. A crash leaves at most a.photosift-*.tmpfile or a copy that is already in the journal. - Source drift is detected. The plan stores each file's SHA-256. If the source differs when applying, the file is refused and left alone.
- Never overwrite. If the destination exists, the plan picks a new sequence number; at apply time an existing destination is a failure, not an overwrite. A destination that already holds identical bytes is planned as
skip, which makes re-planning after an interrupted run safe. - "Delete" is recoverable.
--dupes deletein move mode moves the loser to<dest>/.photosift-trash/<run>/with the same verification. Nothing is unlinked without an intact copy, andundorestores it. Empty the trash folder yourself once you are happy. In copy mode delete entries are skipped.
Journal and undo
apply --execute appends one JSON line per completed file, fsynced, to a journal (default ./photosift-<time>.journal.jsonl):
{"type": "header", "version": 1, "mode": "move", "dest": "/demo/library", "started": "20261016-192403"}
{"type": "op", "op": "move", "src": "/demo/lib/a.jpg", "dest": "/demo/library/2023/a.jpg", "sha256": "...", "size": 1234}
undo walks the journal in reverse:
- copy: remove the destination if it still has the journaled hash.
- move or trash: copy the destination back to its original path (verified), then remove it. If the source reappeared with identical content, the destination is just removed.
- A destination that was modified since the apply is never touched; it is reported as a failure.
- Directories left empty are pruned up to, but not including, the destination root.
Re-running apply on the same plan after an interruption is safe: files whose source is gone and whose destination exists are reported as already applied.
fix-dates
EXIF is written to a temp copy, checked with Pillow (verify plus reading the tag back), then swapped in with os.replace; the original mtime is restored. Only JPEGs lacking an EXIF date whose filename carries one are candidates. This one operation is not journaled, so run it on files you have backed up.
What is not covered
- The journal protects photosift's own operations; it cannot restore a file you modified afterwards.
- Hash verification detects corruption in transit, not on-disk bit rot of the source before the plan was made.