tmp_path nests too deeply for AF_UNIX socket paths (~104-108 byte
limit on macOS). Use a flat tempfile.TemporaryDirectory(), matching
the pattern already used by TestEntryKind::test_socket.
Co-authored-by: Maxim Shulga <maxim.shulga@alludo.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Handle special filesystem entries explicitly
- Added fs.entry_kind(): classifies via lstat() mode bits alone
("file", "dir", "symlink", "fifo", "socket", "block_device",
"char_device", "unknown") - never opens the entry, so classifying a
FIFO can't block even without a writer.
- rsync() now classifies every source entry before touching it and
yields a new Actions.EXCLUDE for anything outside file/dir/symlink,
instead of falling through to copy_direntry()'s file branch, which
would either block forever opening a writerless FIFO for read, or
raise an OSError that initiate_backup() treats as fatal (aborting the
whole backup for one socket).
- initiate_backup() records exclusions in the completion manifest
(keyed by snapshot-relative path, valued with the excluded kind), and
no longer nest_hardlinks or counts a recurring exclusion as a change
worth keeping a new snapshot for.
- Chose the "recorded exclusion" policy the plan allows, rather than
fail-fast: a FIFO or socket living in a backed-up tree is not itself
a backup failure, and previously turned into one.
Note: rsync_ext() (external rsync) is unaffected by this change and
keeps whatever its own --archive flags do with special files; the P1
item on Python/external implementation parity covers reconciling that.
The pre-existing test_src_is_socket failure (AF_UNIX path length on
macOS tmp_path) is untouched here - it's separately tracked under P1
test fixes - but this PR adds working socket/FIFO/char-device coverage
via a short-path tempdir.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Address Copilot review: fix stale exclude, exclusion-only diffs, test portability
- fs.rsync(): remove the existing dst_entry before yielding EXCLUDE for
a src entry with an unsupported kind, so an entry inherited via
hardlink from a prior snapshot doesn't linger in the new one.
- backup.initiate_backup(): compare this run's excluded set against
the previous snapshot's manifest; a change in exclusions alone now
counts as backup_changed, so the updated manifest gets written.
- tests: skip mkfifo/AF_UNIX-dependent tests on platforms without
support, and use TemporaryDirectory/context managers instead of
manual mkdtemp/unlink for more robust cleanup.
* Remove unused shutil import from test_rsync.py
Left over after switching the socket/fifo tests to
tempfile.TemporaryDirectory(), per Copilot's suppressed comment.
* Fix rm_direntry() no-op on special dst entries
rm_direntry() only handled file/symlink/dir - a FIFO, socket, or
device node at dst (e.g. inherited from an external-rsync snapshot)
silently survived removal, since is_file/is_dir/is_symlink all
return False for it. Any non-directory entry is unlinkable the same
way, so invert the branching: recurse for directories, unlink
everything else.
---------
Co-authored-by: Maxim Shulga <maxim.shulga@alludo.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Preserve symlinks and old-snapshot immutability
Two real bugs found while implementing this item, both in fs.py:
- copy_direntry() checked entry.is_dir() (default follow_symlinks=True)
before entry.is_symlink(). A symlink pointing at a directory was
misclassified as a directory and an empty real directory was created
in its place, silently destroying the symlink and losing its target
entirely. Fixed by checking is_symlink() first, and is_dir() with
follow_symlinks=False.
- rsync()'s permission/ownership-sync branch called os.chmod/os.chown
on dst_entry.path unconditionally. Two problems: (1) for a symlink
whose target already matched, this had no follow_symlinks=False
guard and could rewrite an external symlink target's permissions;
(2) for a regular file, that path may still be hardlinked to the
same file in the previous, completed snapshot (hardlink_dir() links
unchanged files in at the start of every backup) - mutating it in
place silently corrupted every older snapshot sharing the inode.
Fixed by skipping symlinks entirely (nothing to sync once their
target matches) and adding _break_shared_hardlink(), a copy-on-write
step that gives a file its own inode before any in-place metadata
change.
Also fixed the dir-mtime-restore loop's is_dir() call (same
default-follow_symlinks issue, lower severity since its own operations
were already lstat-based/no-follow).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Address Copilot review: preserve ownership/timestamps when breaking hardlinks, fix test fd leak
- _break_shared_hardlink() now explicitly restores the original file's
uid/gid and atime/mtime on the new private copy before it replaces
the shared one. copy_file() only carries over mode at create time, so
without this a caller that only meant to update permissions would
silently also reset ownership/mtime as a side effect of the
copy-on-write break itself. Also cleans up the temp file on failure
and uses a less collision-prone temp name.
- Fixed a file descriptor leak in the new hardlink-immutability test
(bare open(...).read() without a context manager)
- Strengthened the same test to assert mtime survives the break
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>
Verify that timestamps, permissions, and ownership are correctly
preserved when copying files, directories, and symlinks. Tests account
for filesystem behavior where reading a file updates its atime by
capturing source timestamps before the copy operation.
Move all rsync-related tests (44 tests) from test_fs.py to new
test_rsync.py module for better organization:
- TestRsync (14 tests)
- TestRsyncBasic (5 tests)
- TestParseRsyncOutput (17 tests)
- TestRsyncExt (8 tests)
Extract shared helper functions to conftest.py to eliminate duplication:
- create_file(), create_dir(), relpath()
- check_identical_file()
- common_fs_dirs fixture
This improves test organization and maintainability by grouping related
tests and removing code duplication between test modules.