Prevent stale, truncated, or changing-file copies (#10)

* Prevent stale, truncated, or changing-file copies

- copy_file() now loops until every byte is actually written (os.write
  may write fewer bytes than given) and compares fstat() on the
  source's own open descriptor before/after the read loop - this
  reflects the underlying inode directly, catching a source file
  truncated, grown, or overwritten in place while being read, and isn't
  fooled by the source path being renamed away mid-copy. Raises
  BackupCreationError (already a fatal, staging-discarding condition in
  initiate_backup) on a mismatch.
- copy_file() returns a sha256 hex digest of the copied content;
  copy_direntry()/update_direntry() propagate it; rsync() carries it
  through the previously-unused msg field for CREATE/REWRITE actions.
- initiate_backup() collects those digests into a checksums dict keyed
  by path relative to the snapshot root, recorded in the completion
  manifest (already JSON since the topology-validation work).
- Added verify_snapshot(): re-hashes every file with a recorded
  checksum and reports any mismatch, satisfying the release gate's
  "completed snapshot passes manifest verification" requirement.
- Switched every mtime comparison and os.utime() restore (rsync's
  file/directory sync, hardlink_dir's directory metadata, the
  hardlink-breaking copy-on-write step) from float seconds to
  nanoseconds, so a same-second modification can't be missed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Address Copilot review: hash checksums for external rsync too, fix log/error text

- initiate_backup() now hashes CREATE/REWRITE files itself when msg is
  empty (fs.rsync_ext(), the external-rsync path, never yields a
  digest). Without this, verify_snapshot() silently became a no-op for
  every snapshot created with external_rsync=True.
- Fixed a pre-existing log message that said "different inodes" for
  the branch whose actual condition is src/dst sharing an inode.
- verify_snapshot()'s error message now also mentions a corrupt
  manifest, not just missing/pre-migration, matching what
  _read_manifest() actually distinguishes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Maxim Shulga <maxim.shulga@alludo.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-08-04 00:10:25 +00:00
1 parent cc61aed10c
commit b4db7c806b
5 files changed
+326 -46

No files matched your search

+5 -5
View File
@@ -74,11 +74,11 @@ Acceptance criteria:
### Prevent stale, truncated, or changing-file copies
- [ ] Handle partial writes correctly or use a proven copy primitive.
- [ ] Use nanosecond timestamps and a documented integrity strategy.
- [ ] Add checksum verification, either always or through an explicit mode.
- [ ] Stat files before and after copying and retry or fail if they changed.
- [ ] Record integrity information in the completion manifest.
- [x] Handle partial writes correctly or use a proven copy primitive.
- [x] Use nanosecond timestamps and a documented integrity strategy.
- [x] Add checksum verification, either always or through an explicit mode.
- [x] Stat files before and after copying and retry or fail if they changed.
- [x] Record integrity information in the completion manifest.
Acceptance criteria: