Handle special filesystem entries explicitly (#11)
* 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>
This commit is contained in:
5 files changed
+233
-15
No files matched your search
@@ -1081,3 +1081,35 @@ def test_external_rsync_snapshot_still_gets_checksums(integration_dirs):
|
||||
manifest = bk._read_manifest(bk._get_backup_marker(snapshot).path)
|
||||
assert manifest["checksums"]
|
||||
assert bk.verify_snapshot(snapshot) == []
|
||||
|
||||
|
||||
@pytest.mark.skipif(not hasattr(os, "mkfifo"),
|
||||
reason="os.mkfifo not available on this platform")
|
||||
def test_fifo_excluded_and_recorded_without_blocking(integration_dirs):
|
||||
"""
|
||||
A FIFO in the source must not block the backup (no writer is ever
|
||||
attached), must not abort it, and must be recorded in the manifest
|
||||
as an exclusion rather than silently disappearing.
|
||||
"""
|
||||
backups_dir, source_dir = integration_dirs
|
||||
(source_dir / "file1.txt").write_text("content1")
|
||||
os.mkfifo(str(source_dir / "a_fifo"))
|
||||
|
||||
snapshot = bk.initiate_backup(
|
||||
sources=[str(source_dir)],
|
||||
backups_dir=str(backups_dir),
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
assert snapshot is not None
|
||||
source_name = os.path.basename(str(source_dir))
|
||||
manifest = bk._read_manifest(bk._get_backup_marker(snapshot).path)
|
||||
assert manifest["excluded"] == {
|
||||
os.path.join(source_name, "a_fifo"): "fifo"
|
||||
}
|
||||
assert not os.path.lexists(
|
||||
os.path.join(snapshot.path, source_name, "a_fifo")
|
||||
)
|
||||
assert os.path.exists(
|
||||
os.path.join(snapshot.path, source_name, "file1.txt")
|
||||
)
|
||||
Reference in new issue
Block a user