From 119245171816e641853e26b8a20d9552907c5e6d Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Thu, 25 Sep 2025 10:24:12 -0700 Subject: [PATCH] Fix snapshotting in the ZFS, Btrfs, and LVM hooks to play nicely with the Borg 1.4+ "slashdot" hack within source directory paths (#1151). --- NEWS | 2 ++ borgmatic/hooks/data_source/btrfs.py | 5 ++++- borgmatic/hooks/data_source/lvm.py | 5 ++++- borgmatic/hooks/data_source/zfs.py | 5 ++++- tests/unit/hooks/data_source/test_btrfs.py | 5 +++++ tests/unit/hooks/data_source/test_lvm.py | 4 ++++ tests/unit/hooks/data_source/test_zfs.py | 4 ++++ 7 files changed, 27 insertions(+), 3 deletions(-) diff --git a/NEWS b/NEWS index dc5fe202..d3e7bcdb 100644 --- a/NEWS +++ b/NEWS @@ -3,6 +3,8 @@ service. * #1149: Add support for Python 3.14. * #1149: Include automated tests in the source dist tarball uploaded to PyPI. + * #1151: Fix snapshotting in the ZFS, Btrfs, and LVM hooks to play nicely with the Borg 1.4+ + "slashdot" hack within source directory paths. 2.0.8 * #1114: Document systemd configuration changes for the ZFS filesystem hook. diff --git a/borgmatic/hooks/data_source/btrfs.py b/borgmatic/hooks/data_source/btrfs.py index 85adb245..3bfe700b 100644 --- a/borgmatic/hooks/data_source/btrfs.py +++ b/borgmatic/hooks/data_source/btrfs.py @@ -265,7 +265,10 @@ def make_borg_snapshot_pattern(subvolume_path, pattern): rewritten_path = initial_caret + os.path.join( subvolume_path, f'{BORGMATIC_SNAPSHOT_PREFIX}{os.getpid()}', - '.', # Borg 1.4+ "slashdot" hack. + # Use the Borg 1.4+ "slashdot" hack to prevent the snapshot path prefix from getting + # included in the archive—but only if there's not already a slashdot hack present in the + # pattern. + ('' if f'{os.path.sep}.{os.path.sep}' in pattern.path else '.'), # Included so that the source directory ends up in the Borg archive at its "original" path. pattern.path.lstrip('^').lstrip(os.path.sep), ) diff --git a/borgmatic/hooks/data_source/lvm.py b/borgmatic/hooks/data_source/lvm.py index 1e5fd266..1d43d224 100644 --- a/borgmatic/hooks/data_source/lvm.py +++ b/borgmatic/hooks/data_source/lvm.py @@ -166,7 +166,10 @@ def make_borg_snapshot_pattern(pattern, logical_volume, normalized_runtime_direc hashlib.shake_256(logical_volume.mount_point.encode('utf-8')).hexdigest( MOUNT_POINT_HASH_LENGTH, ), - '.', # Borg 1.4+ "slashdot" hack. + # Use the Borg 1.4+ "slashdot" hack to prevent the snapshot path prefix from getting + # included in the archive—but only if there's not already a slashdot hack present in the + # pattern. + ('' if f'{os.path.sep}.{os.path.sep}' in pattern.path else '.'), # Included so that the source directory ends up in the Borg archive at its "original" path. pattern.path.lstrip('^').lstrip(os.path.sep), ) diff --git a/borgmatic/hooks/data_source/zfs.py b/borgmatic/hooks/data_source/zfs.py index a9745483..988428c0 100644 --- a/borgmatic/hooks/data_source/zfs.py +++ b/borgmatic/hooks/data_source/zfs.py @@ -214,7 +214,10 @@ def make_borg_snapshot_pattern(pattern, dataset, normalized_runtime_directory): # For instance, without this, snapshotting a dataset at /var and another at /var/spool would # result in overlapping snapshot patterns and therefore colliding mount attempts. hashlib.shake_256(dataset.mount_point.encode('utf-8')).hexdigest(MOUNT_POINT_HASH_LENGTH), - '.', # Borg 1.4+ "slashdot" hack. + # Use the Borg 1.4+ "slashdot" hack to prevent the snapshot path prefix from getting + # included in the archive—but only if there's not already a slashdot hack present in the + # pattern. + ('' if f'{os.path.sep}.{os.path.sep}' in pattern.path else '.'), # Included so that the source directory ends up in the Borg archive at its "original" path. pattern.path.lstrip('^').lstrip(os.path.sep), ) diff --git a/tests/unit/hooks/data_source/test_btrfs.py b/tests/unit/hooks/data_source/test_btrfs.py index 0ad67d0f..81a0cc42 100644 --- a/tests/unit/hooks/data_source/test_btrfs.py +++ b/tests/unit/hooks/data_source/test_btrfs.py @@ -326,6 +326,11 @@ def test_make_snapshot_path_includes_stripped_subvolume_path( ), ('/', Pattern('/foo'), Pattern('/.borgmatic-snapshot-1234/./foo')), ('/', Pattern('/'), Pattern('/.borgmatic-snapshot-1234/./')), + ( + '/foo/bar', + Pattern('/foo/bar/./baz'), + Pattern('/foo/bar/.borgmatic-snapshot-1234/foo/bar/./baz'), + ), ), ) def test_make_borg_snapshot_pattern_includes_slashdot_hack_and_stripped_pattern_path( diff --git a/tests/unit/hooks/data_source/test_lvm.py b/tests/unit/hooks/data_source/test_lvm.py index e692a48c..50ada6f5 100644 --- a/tests/unit/hooks/data_source/test_lvm.py +++ b/tests/unit/hooks/data_source/test_lvm.py @@ -270,6 +270,10 @@ def test_snapshot_logical_volume_with_non_percentage_snapshot_name_uses_lvcreate ), (Pattern('/foo'), Pattern('/run/borgmatic/lvm_snapshots/b33f/./foo')), (Pattern('/'), Pattern('/run/borgmatic/lvm_snapshots/b33f/./')), + ( + Pattern('/foo/./bar/baz'), + Pattern('/run/borgmatic/lvm_snapshots/b33f/foo/./bar/baz'), + ), ), ) def test_make_borg_snapshot_pattern_includes_slashdot_hack_and_stripped_pattern_path( diff --git a/tests/unit/hooks/data_source/test_zfs.py b/tests/unit/hooks/data_source/test_zfs.py index 5d51213a..2af87e3c 100644 --- a/tests/unit/hooks/data_source/test_zfs.py +++ b/tests/unit/hooks/data_source/test_zfs.py @@ -278,6 +278,10 @@ def test_get_all_dataset_mount_points_omits_duplicates(): ), (Pattern('/foo'), Pattern('/run/borgmatic/zfs_snapshots/b33f/./foo')), (Pattern('/'), Pattern('/run/borgmatic/zfs_snapshots/b33f/./')), + ( + Pattern('/foo/./bar/baz'), + Pattern('/run/borgmatic/zfs_snapshots/b33f/foo/./bar/baz'), + ), ), ) def test_make_borg_snapshot_pattern_includes_slashdot_hack_and_stripped_pattern_path(