diff --git a/src/watchdog/utils/dirsnapshot.py b/src/watchdog/utils/dirsnapshot.py index d18dbbe9..40ff95bc 100644 --- a/src/watchdog/utils/dirsnapshot.py +++ b/src/watchdog/utils/dirsnapshot.py @@ -68,15 +68,32 @@ class DirectorySnapshotDiff(object): with the reference snapshot. :type snapshot: :class:`DirectorySnapshot` + :param ignore_device: + A boolean indicating whether to ignore the device id or not. + By default, a file may be uniquely identified by a combination of its first + inode and its device id. The problem is that the device id may (or may not) + change between system boots. This problem would cause the DirectorySnapshotDiff + to think a file has been deleted and created again but it would be the + exact same file. + Set to True only if you are sure you will always use the same device. + :type ignore_device: + :class:`bool` """ - def __init__(self, ref, snapshot): + def __init__(self, ref, snapshot, ignore_device=False): created = snapshot.paths - ref.paths deleted = ref.paths - snapshot.paths + if ignore_device: + def get_inode(directory, full_path): + return directory.inode(full_path)[0] + else: + def get_inode(directory, full_path): + return directory.inode(full_path) + # check that all unchanged paths have the same inode for path in ref.paths & snapshot.paths: - if ref.inode(path) != snapshot.inode(path): + if get_inode(ref, path) != get_inode(snapshot, path): created.add(path) deleted.add(path) @@ -101,7 +118,7 @@ def __init__(self, ref, snapshot): # first check paths that have not moved modified = set() for path in ref.paths & snapshot.paths: - if ref.inode(path) == snapshot.inode(path): + if get_inode(ref, path) == get_inode(snapshot, path): if ref.mtime(path) != snapshot.mtime(path) or ref.size(path) != snapshot.size(path): modified.add(path) diff --git a/tests/test_snapshot_diff.py b/tests/test_snapshot_diff.py index 9c881c3d..9cae27d3 100644 --- a/tests/test_snapshot_diff.py +++ b/tests/test_snapshot_diff.py @@ -156,3 +156,37 @@ def walk(self, root): # Children of a/b/ are no more accessible and so removed in the new snapshot assert diff.dirs_deleted == [(p('a', 'b', 'c'))] + + +def test_ignore_device(monkeypatch, p): + # Create a file and take a snapshot. + touch(p('file')) + ref = DirectorySnapshot(p('')) + wait() + + def inode(self, path): + # This function will always return a different device_id, + # even for the same file. + result = inode_orig(self, path) + inode.times += 1 + return result[0], result[1] + inode.times + inode.times = 0 + + # Set the custom inode function. + inode_orig = DirectorySnapshot.inode + monkeypatch.setattr(DirectorySnapshot, 'inode', inode) + + # If we make the diff of the same directory, since by default the + # DirectorySnapshotDiff compares the snapshots using the device_id (and it will + # be different), it thinks that the same file has been deleted and created again. + snapshot = DirectorySnapshot(p('')) + diff_with_device = DirectorySnapshotDiff(ref, snapshot) + assert diff_with_device.files_deleted == [(p('file'))] + assert diff_with_device.files_created == [(p('file'))] + + # Otherwise, if we choose to ignore the device, the file will not be detected as + # deleted and re-created. + snapshot = DirectorySnapshot(p('')) + diff_without_device = DirectorySnapshotDiff(ref, snapshot, ignore_device=True) + assert diff_without_device.files_deleted == [] + assert diff_without_device.files_created == []