Browse Source

Fix for a runtime directory error when the configured patterns contain a global exclude (#1150).

Dan Helfman 2 weeks ago
parent
commit
dcd567f4f0

+ 1 - 0
NEWS

@@ -5,6 +5,7 @@
    databases when dumping "all".
    databases when dumping "all".
  * #1150: Fix for a runtime directory error when the "create" action is used with the "--log-json"
  * #1150: Fix for a runtime directory error when the "create" action is used with the "--log-json"
    flag.
    flag.
+ * #1150: Fix for a runtime directory error when the configured patterns contain a global exclude.
  * #1161: Fix a traceback (TypeError) in the "check" action with Python 3.14.
  * #1161: Fix a traceback (TypeError) in the "check" action with Python 3.14.
  * Add documentation search.
  * Add documentation search.
  * Change the URL of the local documentation development server to be more like the production URL.
  * Change the URL of the local documentation development server to be more like the production URL.

+ 12 - 8
borgmatic/hooks/data_source/bootstrap.py

@@ -7,6 +7,7 @@ import os
 
 
 import borgmatic.borg.pattern
 import borgmatic.borg.pattern
 import borgmatic.config.paths
 import borgmatic.config.paths
+import borgmatic.hooks.data_source.config
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
@@ -58,20 +59,23 @@ def dump_data_sources(
             manifest_file,
             manifest_file,
         )
         )
 
 
-    patterns.extend(
-        borgmatic.borg.pattern.Pattern(
-            config_path,
-            source=borgmatic.borg.pattern.Pattern_source.HOOK,
-        )
-        for config_path in config_paths
-    )
-    patterns.append(
+    borgmatic.hooks.data_source.config.inject_pattern(
+        patterns,
         borgmatic.borg.pattern.Pattern(
         borgmatic.borg.pattern.Pattern(
             os.path.join(borgmatic_runtime_directory, 'bootstrap'),
             os.path.join(borgmatic_runtime_directory, 'bootstrap'),
             source=borgmatic.borg.pattern.Pattern_source.HOOK,
             source=borgmatic.borg.pattern.Pattern_source.HOOK,
         ),
         ),
     )
     )
 
 
+    for config_path in config_paths:
+        borgmatic.hooks.data_source.config.inject_pattern(
+            patterns,
+            borgmatic.borg.pattern.Pattern(
+                config_path,
+                source=borgmatic.borg.pattern.Pattern_source.HOOK,
+            ),
+        )
+
     return []
     return []
 
 
 
 

+ 5 - 7
borgmatic/hooks/data_source/btrfs.py

@@ -10,6 +10,7 @@ import subprocess
 import borgmatic.borg.pattern
 import borgmatic.borg.pattern
 import borgmatic.config.paths
 import borgmatic.config.paths
 import borgmatic.execute
 import borgmatic.execute
+import borgmatic.hooks.data_source.config
 import borgmatic.hooks.data_source.snapshot
 import borgmatic.hooks.data_source.snapshot
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
@@ -330,14 +331,11 @@ def dump_data_sources(
 
 
         for pattern in subvolume.contained_patterns:
         for pattern in subvolume.contained_patterns:
             snapshot_pattern = make_borg_snapshot_pattern(subvolume.path, pattern)
             snapshot_pattern = make_borg_snapshot_pattern(subvolume.path, pattern)
+            borgmatic.hooks.data_source.config.replace_pattern(patterns, pattern, snapshot_pattern)
 
 
-            # Attempt to update the pattern in place, since pattern order matters to Borg.
-            try:
-                patterns[patterns.index(pattern)] = snapshot_pattern
-            except ValueError:
-                patterns.append(snapshot_pattern)
-
-        patterns.append(make_snapshot_exclude_pattern(subvolume.path))
+        borgmatic.hooks.data_source.config.inject_pattern(
+            patterns, make_snapshot_exclude_pattern(subvolume.path)
+        )
 
 
     return []
     return []
 
 

+ 63 - 0
borgmatic/hooks/data_source/config.py

@@ -3,6 +3,7 @@ import logging
 import shutil
 import shutil
 import subprocess
 import subprocess
 
 
+import borgmatic.borg.pattern
 from borgmatic.execute import execute_command_and_capture_output
 from borgmatic.execute import execute_command_and_capture_output
 
 
 IS_A_HOOK = False
 IS_A_HOOK = False
@@ -102,3 +103,65 @@ def get_ip_from_container(container):
     raise ValueError(
     raise ValueError(
         f"Could not determine ip address for container '{container}'; running in host mode or userspace networking?"
         f"Could not determine ip address for container '{container}'; running in host mode or userspace networking?"
     )
     )
+
+
+def inject_pattern(patterns, data_source_pattern):
+    '''
+    Given a list of borgmatic.borg.pattern.Pattern instances representing the configured patterns,
+    insert the given data source pattern at the start of the list. The idea is that borgmatic is
+    injecting its own custom pattern specific to a data source hook into the user's configured
+    patterns so that the hook's data gets included in the backup.
+
+    As part of this injection, if the data source pattern is a root pattern, also insert an
+    "include" version of the given root pattern, in an attempt to preempt any of the user's
+    configured exclude patterns that may follow.
+    '''
+    if data_source_pattern.type == borgmatic.borg.pattern.Pattern_type.ROOT:
+        patterns.insert(
+            0,
+            borgmatic.borg.pattern.Pattern(
+                path=data_source_pattern.path,
+                type=borgmatic.borg.pattern.Pattern_type.INCLUDE,
+                style=data_source_pattern.style,
+                device=data_source_pattern.device,
+                source=borgmatic.borg.pattern.Pattern_source.HOOK,
+            ),
+        )
+
+    patterns.insert(0, data_source_pattern)
+
+
+def replace_pattern(patterns, pattern_to_replace, data_source_pattern):
+    '''
+    Given a list of borgmatic.borg.pattern.Pattern instances representing the configured patterns,
+    replace the given pattern with the given data source pattern. The idea is that borgmatic is
+    replacing a configured pattern with its own modified pattern specific to a data source hook so
+    that the hook's data gets included in the backup.
+
+    As part of this replacement, if the data source pattern is a root pattern, also insert an
+    "include" version of the given root pattern right after the replaced pattern, in an attempt to
+    preempt any of the user's configured exclude patterns that may follow.
+
+    If the pattern to replace can't be found in the given patterns, then just inject the data source
+    pattern at the start of the list.
+    '''
+    try:
+        index = patterns.index(pattern_to_replace)
+    except ValueError:
+        inject_pattern(patterns, data_source_pattern)
+
+        return
+
+    patterns[index] = data_source_pattern
+
+    if data_source_pattern.type == borgmatic.borg.pattern.Pattern_type.ROOT:
+        patterns.insert(
+            index + 1,
+            borgmatic.borg.pattern.Pattern(
+                path=data_source_pattern.path,
+                type=borgmatic.borg.pattern.Pattern_type.INCLUDE,
+                style=data_source_pattern.style,
+                device=data_source_pattern.device,
+                source=borgmatic.borg.pattern.Pattern_source.HOOK,
+            ),
+        )

+ 2 - 5
borgmatic/hooks/data_source/lvm.py

@@ -10,6 +10,7 @@ import subprocess
 import borgmatic.borg.pattern
 import borgmatic.borg.pattern
 import borgmatic.config.paths
 import borgmatic.config.paths
 import borgmatic.execute
 import borgmatic.execute
+import borgmatic.hooks.data_source.config
 import borgmatic.hooks.data_source.snapshot
 import borgmatic.hooks.data_source.snapshot
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
@@ -275,11 +276,7 @@ def dump_data_sources(
                 normalized_runtime_directory,
                 normalized_runtime_directory,
             )
             )
 
 
-            # Attempt to update the pattern in place, since pattern order matters to Borg.
-            try:
-                patterns[patterns.index(pattern)] = snapshot_pattern
-            except ValueError:
-                patterns.append(snapshot_pattern)
+            borgmatic.hooks.data_source.config.replace_pattern(patterns, pattern, snapshot_pattern)
 
 
     return []
     return []
 
 

+ 3 - 1
borgmatic/hooks/data_source/mariadb.py

@@ -7,6 +7,7 @@ import shlex
 import borgmatic.borg.pattern
 import borgmatic.borg.pattern
 import borgmatic.config.paths
 import borgmatic.config.paths
 import borgmatic.hooks.credential.parse
 import borgmatic.hooks.credential.parse
+import borgmatic.hooks.data_source.config
 from borgmatic.execute import (
 from borgmatic.execute import (
     execute_command,
     execute_command,
     execute_command_and_capture_output,
     execute_command_and_capture_output,
@@ -366,7 +367,8 @@ def dump_data_sources(
         dump.write_data_source_dumps_metadata(
         dump.write_data_source_dumps_metadata(
             borgmatic_runtime_directory, 'mariadb_databases', dumps_metadata
             borgmatic_runtime_directory, 'mariadb_databases', dumps_metadata
         )
         )
-        patterns.append(
+        borgmatic.hooks.data_source.config.inject_pattern(
+            patterns,
             borgmatic.borg.pattern.Pattern(
             borgmatic.borg.pattern.Pattern(
                 os.path.join(borgmatic_runtime_directory, 'mariadb_databases'),
                 os.path.join(borgmatic_runtime_directory, 'mariadb_databases'),
                 source=borgmatic.borg.pattern.Pattern_source.HOOK,
                 source=borgmatic.borg.pattern.Pattern_source.HOOK,

+ 3 - 1
borgmatic/hooks/data_source/mongodb.py

@@ -5,6 +5,7 @@ import shlex
 import borgmatic.borg.pattern
 import borgmatic.borg.pattern
 import borgmatic.config.paths
 import borgmatic.config.paths
 import borgmatic.hooks.credential.parse
 import borgmatic.hooks.credential.parse
+import borgmatic.hooks.data_source.config
 from borgmatic.execute import execute_command, execute_command_with_processes
 from borgmatic.execute import execute_command, execute_command_with_processes
 from borgmatic.hooks.data_source import config as database_config
 from borgmatic.hooks.data_source import config as database_config
 from borgmatic.hooks.data_source import dump
 from borgmatic.hooks.data_source import dump
@@ -100,7 +101,8 @@ def dump_data_sources(
         dump.write_data_source_dumps_metadata(
         dump.write_data_source_dumps_metadata(
             borgmatic_runtime_directory, 'mongodb_databases', dumps_metadata
             borgmatic_runtime_directory, 'mongodb_databases', dumps_metadata
         )
         )
-        patterns.append(
+        borgmatic.hooks.data_source.config.inject_pattern(
+            patterns,
             borgmatic.borg.pattern.Pattern(
             borgmatic.borg.pattern.Pattern(
                 os.path.join(borgmatic_runtime_directory, 'mongodb_databases'),
                 os.path.join(borgmatic_runtime_directory, 'mongodb_databases'),
                 source=borgmatic.borg.pattern.Pattern_source.HOOK,
                 source=borgmatic.borg.pattern.Pattern_source.HOOK,

+ 3 - 1
borgmatic/hooks/data_source/mysql.py

@@ -6,6 +6,7 @@ import shlex
 import borgmatic.borg.pattern
 import borgmatic.borg.pattern
 import borgmatic.config.paths
 import borgmatic.config.paths
 import borgmatic.hooks.credential.parse
 import borgmatic.hooks.credential.parse
+import borgmatic.hooks.data_source.config
 import borgmatic.hooks.data_source.mariadb
 import borgmatic.hooks.data_source.mariadb
 from borgmatic.execute import (
 from borgmatic.execute import (
     execute_command,
     execute_command,
@@ -297,7 +298,8 @@ def dump_data_sources(
         dump.write_data_source_dumps_metadata(
         dump.write_data_source_dumps_metadata(
             borgmatic_runtime_directory, 'mysql_databases', dumps_metadata
             borgmatic_runtime_directory, 'mysql_databases', dumps_metadata
         )
         )
-        patterns.append(
+        borgmatic.hooks.data_source.config.inject_pattern(
+            patterns,
             borgmatic.borg.pattern.Pattern(
             borgmatic.borg.pattern.Pattern(
                 os.path.join(borgmatic_runtime_directory, 'mysql_databases'),
                 os.path.join(borgmatic_runtime_directory, 'mysql_databases'),
                 source=borgmatic.borg.pattern.Pattern_source.HOOK,
                 source=borgmatic.borg.pattern.Pattern_source.HOOK,

+ 3 - 1
borgmatic/hooks/data_source/postgresql.py

@@ -8,6 +8,7 @@ import shlex
 import borgmatic.borg.pattern
 import borgmatic.borg.pattern
 import borgmatic.config.paths
 import borgmatic.config.paths
 import borgmatic.hooks.credential.parse
 import borgmatic.hooks.credential.parse
+import borgmatic.hooks.data_source.config
 from borgmatic.execute import (
 from borgmatic.execute import (
     execute_command,
     execute_command,
     execute_command_and_capture_output,
     execute_command_and_capture_output,
@@ -260,7 +261,8 @@ def dump_data_sources(
         dump.write_data_source_dumps_metadata(
         dump.write_data_source_dumps_metadata(
             borgmatic_runtime_directory, 'postgresql_databases', dumps_metadata
             borgmatic_runtime_directory, 'postgresql_databases', dumps_metadata
         )
         )
-        patterns.append(
+        borgmatic.hooks.data_source.config.inject_pattern(
+            patterns,
             borgmatic.borg.pattern.Pattern(
             borgmatic.borg.pattern.Pattern(
                 os.path.join(borgmatic_runtime_directory, 'postgresql_databases'),
                 os.path.join(borgmatic_runtime_directory, 'postgresql_databases'),
                 source=borgmatic.borg.pattern.Pattern_source.HOOK,
                 source=borgmatic.borg.pattern.Pattern_source.HOOK,

+ 3 - 1
borgmatic/hooks/data_source/sqlite.py

@@ -4,6 +4,7 @@ import shlex
 
 
 import borgmatic.borg.pattern
 import borgmatic.borg.pattern
 import borgmatic.config.paths
 import borgmatic.config.paths
+import borgmatic.hooks.data_source.config
 from borgmatic.execute import execute_command, execute_command_with_processes
 from borgmatic.execute import execute_command, execute_command_with_processes
 from borgmatic.hooks.data_source import dump
 from borgmatic.hooks.data_source import dump
 
 
@@ -106,7 +107,8 @@ def dump_data_sources(
         dump.write_data_source_dumps_metadata(
         dump.write_data_source_dumps_metadata(
             borgmatic_runtime_directory, 'sqlite_databases', dumps_metadata
             borgmatic_runtime_directory, 'sqlite_databases', dumps_metadata
         )
         )
-        patterns.append(
+        borgmatic.hooks.data_source.config.inject_pattern(
+            patterns,
             borgmatic.borg.pattern.Pattern(
             borgmatic.borg.pattern.Pattern(
                 os.path.join(borgmatic_runtime_directory, 'sqlite_databases'),
                 os.path.join(borgmatic_runtime_directory, 'sqlite_databases'),
                 source=borgmatic.borg.pattern.Pattern_source.HOOK,
                 source=borgmatic.borg.pattern.Pattern_source.HOOK,

+ 2 - 5
borgmatic/hooks/data_source/zfs.py

@@ -9,6 +9,7 @@ import subprocess
 import borgmatic.borg.pattern
 import borgmatic.borg.pattern
 import borgmatic.config.paths
 import borgmatic.config.paths
 import borgmatic.execute
 import borgmatic.execute
+import borgmatic.hooks.data_source.config
 import borgmatic.hooks.data_source.snapshot
 import borgmatic.hooks.data_source.snapshot
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
@@ -306,11 +307,7 @@ def dump_data_sources(
                 normalized_runtime_directory,
                 normalized_runtime_directory,
             )
             )
 
 
-            # Attempt to update the pattern in place, since pattern order matters to Borg.
-            try:
-                patterns[patterns.index(pattern)] = snapshot_pattern
-            except ValueError:
-                patterns.append(snapshot_pattern)
+            borgmatic.hooks.data_source.config.replace_pattern(patterns, pattern, snapshot_pattern)
 
 
     return []
     return []
 
 

+ 59 - 0
tests/integration/hooks/data_source/test_btrfs.py

@@ -0,0 +1,59 @@
+from flexmock import flexmock
+
+from borgmatic.borg.pattern import Pattern, Pattern_style, Pattern_type
+from borgmatic.hooks.data_source import btrfs as module
+
+
+def test_dump_data_sources_snapshots_each_subvolume_and_updates_patterns():
+    patterns = [Pattern('/foo'), Pattern('/mnt/subvol1'), Pattern('/mnt/subvol2')]
+    config = {'btrfs': {}}
+    flexmock(module).should_receive('get_subvolumes').and_return(
+        (
+            module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),
+            module.Subvolume('/mnt/subvol2', contained_patterns=(Pattern('/mnt/subvol2'),)),
+        ),
+    )
+    flexmock(module.os).should_receive('getpid').and_return(1234)
+    flexmock(module).should_receive('snapshot_subvolume').with_args(
+        'btrfs',
+        '/mnt/subvol1',
+        '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
+    ).once()
+    flexmock(module).should_receive('snapshot_subvolume').with_args(
+        'btrfs',
+        '/mnt/subvol2',
+        '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
+    ).once()
+
+    assert (
+        module.dump_data_sources(
+            hook_config=config['btrfs'],
+            config=config,
+            config_paths=('test.yaml',),
+            borgmatic_runtime_directory='/run/borgmatic',
+            patterns=patterns,
+            dry_run=False,
+        )
+        == []
+    )
+
+    assert patterns == [
+        Pattern(
+            '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2/.borgmatic-snapshot-1234',
+            Pattern_type.NO_RECURSE,
+            Pattern_style.FNMATCH,
+        ),
+        Pattern(
+            '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1/.borgmatic-snapshot-1234',
+            Pattern_type.NO_RECURSE,
+            Pattern_style.FNMATCH,
+        ),
+        Pattern('/foo'),
+        Pattern('/mnt/subvol1/.borgmatic-snapshot-1234/./mnt/subvol1'),
+        Pattern('/mnt/subvol1/.borgmatic-snapshot-1234/./mnt/subvol1', Pattern_type.INCLUDE),
+        Pattern('/mnt/subvol2/.borgmatic-snapshot-1234/./mnt/subvol2'),
+        Pattern('/mnt/subvol2/.borgmatic-snapshot-1234/./mnt/subvol2', Pattern_type.INCLUDE),
+    ]
+    assert config == {
+        'btrfs': {},
+    }

+ 81 - 0
tests/integration/hooks/data_source/test_lvm.py

@@ -0,0 +1,81 @@
+from flexmock import flexmock
+
+from borgmatic.borg.pattern import Pattern, Pattern_type
+from borgmatic.hooks.data_source import lvm as module
+
+
+def test_dump_data_sources_snapshots_and_mounts_and_updates_patterns():
+    config = {'lvm': {}}
+    patterns = [Pattern('/mnt/lvolume1/subdir'), Pattern('/mnt/lvolume2')]
+    logical_volumes = (
+        module.Logical_volume(
+            name='lvolume1',
+            device_path='/dev/lvolume1',
+            mount_point='/mnt/lvolume1',
+            contained_patterns=(Pattern('/mnt/lvolume1/subdir'),),
+        ),
+        module.Logical_volume(
+            name='lvolume2',
+            device_path='/dev/lvolume2',
+            mount_point='/mnt/lvolume2',
+            contained_patterns=(Pattern('/mnt/lvolume2'),),
+        ),
+    )
+    flexmock(module).should_receive('get_logical_volumes').and_return(logical_volumes)
+    flexmock(module.os).should_receive('getpid').and_return(1234)
+    flexmock(module).should_receive('snapshot_logical_volume').with_args(
+        'lvcreate',
+        'lvolume1_borgmatic-1234',
+        '/dev/lvolume1',
+        module.DEFAULT_SNAPSHOT_SIZE,
+    ).once()
+    flexmock(module).should_receive('snapshot_logical_volume').with_args(
+        'lvcreate',
+        'lvolume2_borgmatic-1234',
+        '/dev/lvolume2',
+        module.DEFAULT_SNAPSHOT_SIZE,
+    ).once()
+    flexmock(module).should_receive('get_snapshots').with_args(
+        'lvs',
+        snapshot_name='lvolume1_borgmatic-1234',
+    ).and_return(
+        (module.Snapshot(name='lvolume1_borgmatic-1234', device_path='/dev/lvolume1_snap'),),
+    )
+    flexmock(module).should_receive('get_snapshots').with_args(
+        'lvs',
+        snapshot_name='lvolume2_borgmatic-1234',
+    ).and_return(
+        (module.Snapshot(name='lvolume2_borgmatic-1234', device_path='/dev/lvolume2_snap'),),
+    )
+    flexmock(module.hashlib).should_receive('shake_256').and_return(
+        flexmock(hexdigest=lambda length: 'b33f'),
+    )
+    flexmock(module).should_receive('mount_snapshot').with_args(
+        'mount',
+        '/dev/lvolume1_snap',
+        '/run/borgmatic/lvm_snapshots/b33f/mnt/lvolume1',
+    ).once()
+    flexmock(module).should_receive('mount_snapshot').with_args(
+        'mount',
+        '/dev/lvolume2_snap',
+        '/run/borgmatic/lvm_snapshots/b33f/mnt/lvolume2',
+    ).once()
+
+    assert (
+        module.dump_data_sources(
+            hook_config=config['lvm'],
+            config=config,
+            config_paths=('test.yaml',),
+            borgmatic_runtime_directory='/run/borgmatic',
+            patterns=patterns,
+            dry_run=False,
+        )
+        == []
+    )
+
+    assert patterns == [
+        Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume1/subdir'),
+        Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume1/subdir', Pattern_type.INCLUDE),
+        Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2'),
+        Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2', Pattern_type.INCLUDE),
+    ]

+ 48 - 0
tests/integration/hooks/data_source/test_zfs.py

@@ -0,0 +1,48 @@
+import os
+
+from flexmock import flexmock
+
+from borgmatic.borg.pattern import Pattern, Pattern_type
+from borgmatic.hooks.data_source import zfs as module
+
+
+def test_dump_data_sources_snapshots_and_mounts_and_updates_patterns():
+    dataset = flexmock(
+        name='dataset',
+        mount_point='/mnt/dataset',
+        contained_patterns=(Pattern('/mnt/dataset/subdir'),),
+    )
+    flexmock(module).should_receive('get_datasets_to_backup').and_return((dataset,))
+    flexmock(module.os).should_receive('getpid').and_return(1234)
+    full_snapshot_name = 'dataset@borgmatic-1234'
+    flexmock(module).should_receive('snapshot_dataset').with_args(
+        'zfs',
+        full_snapshot_name,
+    ).once()
+    flexmock(module.hashlib).should_receive('shake_256').and_return(
+        flexmock(hexdigest=lambda length: 'b33f'),
+    )
+    snapshot_mount_path = '/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset'
+    flexmock(module).should_receive('mount_snapshot').with_args(
+        'mount',
+        full_snapshot_name,
+        module.os.path.normpath(snapshot_mount_path),
+    ).once()
+    patterns = [Pattern('/mnt/dataset/subdir')]
+
+    assert (
+        module.dump_data_sources(
+            hook_config={},
+            config={'source_directories': '/mnt/dataset', 'zfs': {}},
+            config_paths=('test.yaml',),
+            borgmatic_runtime_directory='/run/borgmatic',
+            patterns=patterns,
+            dry_run=False,
+        )
+        == []
+    )
+
+    assert patterns == [
+        Pattern(os.path.join(snapshot_mount_path, 'subdir')),
+        Pattern(os.path.join(snapshot_mount_path, 'subdir'), Pattern_type.INCLUDE),
+    ]

+ 14 - 1
tests/unit/hooks/data_source/test_bootstrap.py

@@ -22,6 +22,18 @@ def test_dump_data_sources_creates_manifest_file():
         {'borgmatic_version': '1.0.0', 'config_paths': ('test.yaml',)},
         {'borgmatic_version': '1.0.0', 'config_paths': ('test.yaml',)},
         manifest_file,
         manifest_file,
     ).once()
     ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/bootstrap', source=module.borgmatic.borg.pattern.Pattern_source.HOOK
+        ),
+    ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            'test.yaml', source=module.borgmatic.borg.pattern.Pattern_source.HOOK
+        ),
+    ).once()
 
 
     module.dump_data_sources(
     module.dump_data_sources(
         hook_config=None,
         hook_config=None,
@@ -36,6 +48,7 @@ def test_dump_data_sources_creates_manifest_file():
 def test_dump_data_sources_with_store_config_files_false_does_not_create_manifest_file():
 def test_dump_data_sources_with_store_config_files_false_does_not_create_manifest_file():
     flexmock(module.os).should_receive('makedirs').never()
     flexmock(module.os).should_receive('makedirs').never()
     flexmock(module.json).should_receive('dump').never()
     flexmock(module.json).should_receive('dump').never()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').never()
     hook_config = {'store_config_files': False}
     hook_config = {'store_config_files': False}
 
 
     module.dump_data_sources(
     module.dump_data_sources(
@@ -51,7 +64,7 @@ def test_dump_data_sources_with_store_config_files_false_does_not_create_manifes
 def test_dump_data_sources_with_dry_run_does_not_create_manifest_file():
 def test_dump_data_sources_with_dry_run_does_not_create_manifest_file():
     flexmock(module.os).should_receive('makedirs').never()
     flexmock(module.os).should_receive('makedirs').never()
     flexmock(module.json).should_receive('dump').never()
     flexmock(module.json).should_receive('dump').never()
-
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').never()
     module.dump_data_sources(
     module.dump_data_sources(
         hook_config=None,
         hook_config=None,
         config={},
         config={},

+ 182 - 202
tests/unit/hooks/data_source/test_btrfs.py

@@ -399,8 +399,8 @@ def test_make_borg_snapshot_pattern_includes_slashdot_hack_and_stripped_pattern_
     assert module.make_borg_snapshot_pattern(subvolume_path, pattern) == expected_pattern
     assert module.make_borg_snapshot_pattern(subvolume_path, pattern) == expected_pattern
 
 
 
 
-def test_dump_data_sources_snapshots_each_subvolume_and_updates_patterns():
-    patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
+def test_dump_data_sources_snapshots_each_subvolume_and_replaces_patterns():
+    patterns = [Pattern('/foo'), Pattern('/mnt/subvol1'), Pattern('/mnt/subvol2')]
     config = {'btrfs': {}}
     config = {'btrfs': {}}
     flexmock(module).should_receive('get_subvolumes').and_return(
     flexmock(module).should_receive('get_subvolumes').and_return(
         (
         (
@@ -409,26 +409,26 @@ def test_dump_data_sources_snapshots_each_subvolume_and_updates_patterns():
         ),
         ),
     )
     )
     flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
     flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
-        '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
+        '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
     )
     )
     flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
     flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
-        '/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
+        '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
     )
     )
     flexmock(module).should_receive('snapshot_subvolume').with_args(
     flexmock(module).should_receive('snapshot_subvolume').with_args(
         'btrfs',
         'btrfs',
         '/mnt/subvol1',
         '/mnt/subvol1',
-        '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
+        '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
     ).once()
     ).once()
     flexmock(module).should_receive('snapshot_subvolume').with_args(
     flexmock(module).should_receive('snapshot_subvolume').with_args(
         'btrfs',
         'btrfs',
         '/mnt/subvol2',
         '/mnt/subvol2',
-        '/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
+        '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
     ).once()
     ).once()
     flexmock(module).should_receive('make_snapshot_exclude_pattern').with_args(
     flexmock(module).should_receive('make_snapshot_exclude_pattern').with_args(
         '/mnt/subvol1',
         '/mnt/subvol1',
     ).and_return(
     ).and_return(
         Pattern(
         Pattern(
-            '/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234',
+            '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1/.borgmatic-snapshot-1234',
             Pattern_type.NO_RECURSE,
             Pattern_type.NO_RECURSE,
             Pattern_style.FNMATCH,
             Pattern_style.FNMATCH,
         ),
         ),
@@ -437,7 +437,7 @@ def test_dump_data_sources_snapshots_each_subvolume_and_updates_patterns():
         '/mnt/subvol2',
         '/mnt/subvol2',
     ).and_return(
     ).and_return(
         Pattern(
         Pattern(
-            '/mnt/subvol2/.borgmatic-1234/mnt/subvol2/.borgmatic-1234',
+            '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2/.borgmatic-snapshot-1234',
             Pattern_type.NO_RECURSE,
             Pattern_type.NO_RECURSE,
             Pattern_style.FNMATCH,
             Pattern_style.FNMATCH,
         ),
         ),
@@ -445,11 +445,43 @@ def test_dump_data_sources_snapshots_each_subvolume_and_updates_patterns():
     flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
     flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
         '/mnt/subvol1',
         '/mnt/subvol1',
         object,
         object,
-    ).and_return(Pattern('/mnt/subvol1/.borgmatic-1234/mnt/subvol1'))
+    ).and_return(Pattern('/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1'))
     flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
     flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
         '/mnt/subvol2',
         '/mnt/subvol2',
         object,
         object,
-    ).and_return(Pattern('/mnt/subvol2/.borgmatic-1234/mnt/subvol2'))
+    ).and_return(Pattern('/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2'))
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
+        object,
+        Pattern('/mnt/subvol1'),
+        module.borgmatic.borg.pattern.Pattern(
+            '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
+        object,
+        Pattern('/mnt/subvol2'),
+        module.borgmatic.borg.pattern.Pattern(
+            '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1/.borgmatic-snapshot-1234',
+            Pattern_type.NO_RECURSE,
+            Pattern_style.FNMATCH,
+        ),
+    ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2/.borgmatic-snapshot-1234',
+            Pattern_type.NO_RECURSE,
+            Pattern_style.FNMATCH,
+        ),
+    ).once()
 
 
     assert (
     assert (
         module.dump_data_sources(
         module.dump_data_sources(
@@ -463,21 +495,6 @@ def test_dump_data_sources_snapshots_each_subvolume_and_updates_patterns():
         == []
         == []
     )
     )
 
 
-    assert patterns == [
-        Pattern('/foo'),
-        Pattern('/mnt/subvol1/.borgmatic-1234/mnt/subvol1'),
-        Pattern(
-            '/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234',
-            Pattern_type.NO_RECURSE,
-            Pattern_style.FNMATCH,
-        ),
-        Pattern('/mnt/subvol2/.borgmatic-1234/mnt/subvol2'),
-        Pattern(
-            '/mnt/subvol2/.borgmatic-1234/mnt/subvol2/.borgmatic-1234',
-            Pattern_type.NO_RECURSE,
-            Pattern_style.FNMATCH,
-        ),
-    ]
     assert config == {
     assert config == {
         'btrfs': {},
         'btrfs': {},
     }
     }
@@ -490,18 +507,18 @@ def test_dump_data_sources_uses_custom_btrfs_command_in_commands():
         (module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),),
         (module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),),
     )
     )
     flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
     flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
-        '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
+        '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
     )
     )
     flexmock(module).should_receive('snapshot_subvolume').with_args(
     flexmock(module).should_receive('snapshot_subvolume').with_args(
         '/usr/local/bin/btrfs',
         '/usr/local/bin/btrfs',
         '/mnt/subvol1',
         '/mnt/subvol1',
-        '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
+        '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
     ).once()
     ).once()
     flexmock(module).should_receive('make_snapshot_exclude_pattern').with_args(
     flexmock(module).should_receive('make_snapshot_exclude_pattern').with_args(
         '/mnt/subvol1',
         '/mnt/subvol1',
     ).and_return(
     ).and_return(
         Pattern(
         Pattern(
-            '/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234',
+            '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1/.borgmatic-snapshot-1234',
             Pattern_type.NO_RECURSE,
             Pattern_type.NO_RECURSE,
             Pattern_style.FNMATCH,
             Pattern_style.FNMATCH,
         ),
         ),
@@ -509,7 +526,23 @@ def test_dump_data_sources_uses_custom_btrfs_command_in_commands():
     flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
     flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
         '/mnt/subvol1',
         '/mnt/subvol1',
         object,
         object,
-    ).and_return(Pattern('/mnt/subvol1/.borgmatic-1234/mnt/subvol1'))
+    ).and_return(Pattern('/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1'))
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
+        object,
+        Pattern('/mnt/subvol1'),
+        module.borgmatic.borg.pattern.Pattern(
+            '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1/.borgmatic-snapshot-1234',
+            Pattern_type.NO_RECURSE,
+            Pattern_style.FNMATCH,
+        ),
+    ).once()
 
 
     assert (
     assert (
         module.dump_data_sources(
         module.dump_data_sources(
@@ -523,15 +556,6 @@ def test_dump_data_sources_uses_custom_btrfs_command_in_commands():
         == []
         == []
     )
     )
 
 
-    assert patterns == [
-        Pattern('/foo'),
-        Pattern('/mnt/subvol1/.borgmatic-1234/mnt/subvol1'),
-        Pattern(
-            '/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234',
-            Pattern_type.NO_RECURSE,
-            Pattern_style.FNMATCH,
-        ),
-    ]
     assert config == {
     assert config == {
         'btrfs': {
         'btrfs': {
             'btrfs_command': '/usr/local/bin/btrfs',
             'btrfs_command': '/usr/local/bin/btrfs',
@@ -550,18 +574,18 @@ def test_dump_data_sources_with_findmnt_command_warns():
         (module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),),
         (module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),),
     ).once()
     ).once()
     flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
     flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
-        '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
+        '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
     )
     )
     flexmock(module).should_receive('snapshot_subvolume').with_args(
     flexmock(module).should_receive('snapshot_subvolume').with_args(
         'btrfs',
         'btrfs',
         '/mnt/subvol1',
         '/mnt/subvol1',
-        '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
+        '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
     ).once()
     ).once()
     flexmock(module).should_receive('make_snapshot_exclude_pattern').with_args(
     flexmock(module).should_receive('make_snapshot_exclude_pattern').with_args(
         '/mnt/subvol1',
         '/mnt/subvol1',
     ).and_return(
     ).and_return(
         Pattern(
         Pattern(
-            '/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234',
+            '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1/.borgmatic-snapshot-1234',
             Pattern_type.NO_RECURSE,
             Pattern_type.NO_RECURSE,
             Pattern_style.FNMATCH,
             Pattern_style.FNMATCH,
         ),
         ),
@@ -569,7 +593,23 @@ def test_dump_data_sources_with_findmnt_command_warns():
     flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
     flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
         '/mnt/subvol1',
         '/mnt/subvol1',
         object,
         object,
-    ).and_return(Pattern('/mnt/subvol1/.borgmatic-1234/mnt/subvol1'))
+    ).and_return(Pattern('/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1'))
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
+        object,
+        Pattern('/mnt/subvol1'),
+        module.borgmatic.borg.pattern.Pattern(
+            '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1/.borgmatic-snapshot-1234',
+            Pattern_type.NO_RECURSE,
+            Pattern_style.FNMATCH,
+        ),
+    ).once()
 
 
     assert (
     assert (
         module.dump_data_sources(
         module.dump_data_sources(
@@ -583,15 +623,6 @@ def test_dump_data_sources_with_findmnt_command_warns():
         == []
         == []
     )
     )
 
 
-    assert patterns == [
-        Pattern('/foo'),
-        Pattern('/mnt/subvol1/.borgmatic-1234/mnt/subvol1'),
-        Pattern(
-            '/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234',
-            Pattern_type.NO_RECURSE,
-            Pattern_style.FNMATCH,
-        ),
-    ]
     assert config == {
     assert config == {
         'btrfs': {
         'btrfs': {
             'findmnt_command': '/usr/local/bin/findmnt',
             'findmnt_command': '/usr/local/bin/findmnt',
@@ -606,10 +637,12 @@ def test_dump_data_sources_with_dry_run_skips_snapshot_and_patterns_update():
         (module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),),
         (module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),),
     )
     )
     flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
     flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
-        '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
+        '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
     )
     )
     flexmock(module).should_receive('snapshot_subvolume').never()
     flexmock(module).should_receive('snapshot_subvolume').never()
     flexmock(module).should_receive('make_snapshot_exclude_pattern').never()
     flexmock(module).should_receive('make_snapshot_exclude_pattern').never()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').never()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').never()
 
 
     assert (
     assert (
         module.dump_data_sources(
         module.dump_data_sources(
@@ -623,7 +656,6 @@ def test_dump_data_sources_with_dry_run_skips_snapshot_and_patterns_update():
         == []
         == []
     )
     )
 
 
-    assert patterns == [Pattern('/foo'), Pattern('/mnt/subvol1')]
     assert config == {'btrfs': {}}
     assert config == {'btrfs': {}}
 
 
 
 
@@ -634,6 +666,8 @@ def test_dump_data_sources_without_matching_subvolumes_skips_snapshot_and_patter
     flexmock(module).should_receive('make_snapshot_path').never()
     flexmock(module).should_receive('make_snapshot_path').never()
     flexmock(module).should_receive('snapshot_subvolume').never()
     flexmock(module).should_receive('snapshot_subvolume').never()
     flexmock(module).should_receive('make_snapshot_exclude_pattern').never()
     flexmock(module).should_receive('make_snapshot_exclude_pattern').never()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').never()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').never()
 
 
     assert (
     assert (
         module.dump_data_sources(
         module.dump_data_sources(
@@ -647,95 +681,9 @@ def test_dump_data_sources_without_matching_subvolumes_skips_snapshot_and_patter
         == []
         == []
     )
     )
 
 
-    assert patterns == [Pattern('/foo'), Pattern('/mnt/subvol1')]
     assert config == {'btrfs': {}}
     assert config == {'btrfs': {}}
 
 
 
 
-def test_dump_data_sources_snapshots_adds_to_existing_exclude_patterns():
-    patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
-    config = {'btrfs': {}, 'exclude_patterns': ['/bar']}
-    flexmock(module).should_receive('get_subvolumes').and_return(
-        (
-            module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),
-            module.Subvolume('/mnt/subvol2', contained_patterns=(Pattern('/mnt/subvol2'),)),
-        ),
-    )
-    flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
-        '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
-    )
-    flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
-        '/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
-    )
-    flexmock(module).should_receive('snapshot_subvolume').with_args(
-        'btrfs',
-        '/mnt/subvol1',
-        '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
-    ).once()
-    flexmock(module).should_receive('snapshot_subvolume').with_args(
-        'btrfs',
-        '/mnt/subvol2',
-        '/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
-    ).once()
-    flexmock(module).should_receive('make_snapshot_exclude_pattern').with_args(
-        '/mnt/subvol1',
-    ).and_return(
-        Pattern(
-            '/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234',
-            Pattern_type.NO_RECURSE,
-            Pattern_style.FNMATCH,
-        ),
-    )
-    flexmock(module).should_receive('make_snapshot_exclude_pattern').with_args(
-        '/mnt/subvol2',
-    ).and_return(
-        Pattern(
-            '/mnt/subvol2/.borgmatic-1234/mnt/subvol2/.borgmatic-1234',
-            Pattern_type.NO_RECURSE,
-            Pattern_style.FNMATCH,
-        ),
-    )
-    flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
-        '/mnt/subvol1',
-        object,
-    ).and_return(Pattern('/mnt/subvol1/.borgmatic-1234/mnt/subvol1'))
-    flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
-        '/mnt/subvol2',
-        object,
-    ).and_return(Pattern('/mnt/subvol2/.borgmatic-1234/mnt/subvol2'))
-
-    assert (
-        module.dump_data_sources(
-            hook_config=config['btrfs'],
-            config=config,
-            config_paths=('test.yaml',),
-            borgmatic_runtime_directory='/run/borgmatic',
-            patterns=patterns,
-            dry_run=False,
-        )
-        == []
-    )
-
-    assert patterns == [
-        Pattern('/foo'),
-        Pattern('/mnt/subvol1/.borgmatic-1234/mnt/subvol1'),
-        Pattern(
-            '/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234',
-            Pattern_type.NO_RECURSE,
-            Pattern_style.FNMATCH,
-        ),
-        Pattern('/mnt/subvol2/.borgmatic-1234/mnt/subvol2'),
-        Pattern(
-            '/mnt/subvol2/.borgmatic-1234/mnt/subvol2/.borgmatic-1234',
-            Pattern_type.NO_RECURSE,
-            Pattern_style.FNMATCH,
-        ),
-    ]
-    assert config == {
-        'btrfs': {},
-        'exclude_patterns': ['/bar'],
-    }
-
-
 def test_remove_data_source_dumps_deletes_snapshots():
 def test_remove_data_source_dumps_deletes_snapshots():
     config = {'btrfs': {}}
     config = {'btrfs': {}}
     flexmock(module).should_receive('get_subvolumes').and_return(
     flexmock(module).should_receive('get_subvolumes').and_return(
@@ -745,84 +693,90 @@ def test_remove_data_source_dumps_deletes_snapshots():
         ),
         ),
     )
     )
     flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
     flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
-        '/mnt/subvol1/.borgmatic-1234/./mnt/subvol1',
+        '/mnt/subvol1/.borgmatic-snapshot-1234/./mnt/subvol1',
     )
     )
     flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
     flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
-        '/mnt/subvol2/.borgmatic-1234/./mnt/subvol2',
+        '/mnt/subvol2/.borgmatic-snapshot-1234/./mnt/subvol2',
     )
     )
     flexmock(module.borgmatic.config.paths).should_receive(
     flexmock(module.borgmatic.config.paths).should_receive(
         'replace_temporary_subdirectory_with_glob',
         'replace_temporary_subdirectory_with_glob',
     ).with_args(
     ).with_args(
-        '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
+        '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
         temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
         temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
     ).and_return('/mnt/subvol1/.borgmatic-*/mnt/subvol1')
     ).and_return('/mnt/subvol1/.borgmatic-*/mnt/subvol1')
     flexmock(module.borgmatic.config.paths).should_receive(
     flexmock(module.borgmatic.config.paths).should_receive(
         'replace_temporary_subdirectory_with_glob',
         'replace_temporary_subdirectory_with_glob',
     ).with_args(
     ).with_args(
-        '/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
+        '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
         temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
         temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
     ).and_return('/mnt/subvol2/.borgmatic-*/mnt/subvol2')
     ).and_return('/mnt/subvol2/.borgmatic-*/mnt/subvol2')
     flexmock(module.glob).should_receive('glob').with_args(
     flexmock(module.glob).should_receive('glob').with_args(
         '/mnt/subvol1/.borgmatic-*/mnt/subvol1',
         '/mnt/subvol1/.borgmatic-*/mnt/subvol1',
     ).and_return(
     ).and_return(
-        ('/mnt/subvol1/.borgmatic-1234/mnt/subvol1', '/mnt/subvol1/.borgmatic-5678/mnt/subvol1'),
+        (
+            '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
+            '/mnt/subvol1/.borgmatic-snapshot-5678/mnt/subvol1',
+        ),
     )
     )
     flexmock(module.glob).should_receive('glob').with_args(
     flexmock(module.glob).should_receive('glob').with_args(
         '/mnt/subvol2/.borgmatic-*/mnt/subvol2',
         '/mnt/subvol2/.borgmatic-*/mnt/subvol2',
     ).and_return(
     ).and_return(
-        ('/mnt/subvol2/.borgmatic-1234/mnt/subvol2', '/mnt/subvol2/.borgmatic-5678/mnt/subvol2'),
+        (
+            '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
+            '/mnt/subvol2/.borgmatic-snapshot-5678/mnt/subvol2',
+        ),
     )
     )
     flexmock(module.os.path).should_receive('isdir').with_args(
     flexmock(module.os.path).should_receive('isdir').with_args(
-        '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
+        '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
     ).and_return(True)
     ).and_return(True)
     flexmock(module.os.path).should_receive('isdir').with_args(
     flexmock(module.os.path).should_receive('isdir').with_args(
-        '/mnt/subvol1/.borgmatic-5678/mnt/subvol1',
+        '/mnt/subvol1/.borgmatic-snapshot-5678/mnt/subvol1',
     ).and_return(True)
     ).and_return(True)
     flexmock(module.os.path).should_receive('isdir').with_args(
     flexmock(module.os.path).should_receive('isdir').with_args(
-        '/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
+        '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
     ).and_return(True)
     ).and_return(True)
     flexmock(module.os.path).should_receive('isdir').with_args(
     flexmock(module.os.path).should_receive('isdir').with_args(
-        '/mnt/subvol2/.borgmatic-5678/mnt/subvol2',
+        '/mnt/subvol2/.borgmatic-snapshot-5678/mnt/subvol2',
     ).and_return(False)
     ).and_return(False)
     flexmock(module).should_receive('delete_snapshot').with_args(
     flexmock(module).should_receive('delete_snapshot').with_args(
         'btrfs',
         'btrfs',
-        '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
+        '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
     ).once()
     ).once()
     flexmock(module).should_receive('delete_snapshot').with_args(
     flexmock(module).should_receive('delete_snapshot').with_args(
         'btrfs',
         'btrfs',
-        '/mnt/subvol1/.borgmatic-5678/mnt/subvol1',
+        '/mnt/subvol1/.borgmatic-snapshot-5678/mnt/subvol1',
     ).once()
     ).once()
     flexmock(module).should_receive('delete_snapshot').with_args(
     flexmock(module).should_receive('delete_snapshot').with_args(
         'btrfs',
         'btrfs',
-        '/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
+        '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
     ).once()
     ).once()
     flexmock(module).should_receive('delete_snapshot').with_args(
     flexmock(module).should_receive('delete_snapshot').with_args(
         'btrfs',
         'btrfs',
-        '/mnt/subvol2/.borgmatic-5678/mnt/subvol2',
+        '/mnt/subvol2/.borgmatic-snapshot-5678/mnt/subvol2',
     ).never()
     ).never()
     flexmock(module.os.path).should_receive('isdir').with_args(
     flexmock(module.os.path).should_receive('isdir').with_args(
-        '/mnt/subvol1/.borgmatic-1234',
+        '/mnt/subvol1/.borgmatic-snapshot-1234',
     ).and_return(True)
     ).and_return(True)
     flexmock(module.os.path).should_receive('isdir').with_args(
     flexmock(module.os.path).should_receive('isdir').with_args(
-        '/mnt/subvol1/.borgmatic-5678',
+        '/mnt/subvol1/.borgmatic-snapshot-5678',
     ).and_return(True)
     ).and_return(True)
     flexmock(module.os.path).should_receive('isdir').with_args(
     flexmock(module.os.path).should_receive('isdir').with_args(
-        '/mnt/subvol2/.borgmatic-1234',
+        '/mnt/subvol2/.borgmatic-snapshot-1234',
     ).and_return(True)
     ).and_return(True)
     flexmock(module.os.path).should_receive('isdir').with_args(
     flexmock(module.os.path).should_receive('isdir').with_args(
-        '/mnt/subvol2/.borgmatic-5678',
+        '/mnt/subvol2/.borgmatic-snapshot-5678',
     ).and_return(True)
     ).and_return(True)
     flexmock(module.shutil).should_receive('rmtree').with_args(
     flexmock(module.shutil).should_receive('rmtree').with_args(
-        '/mnt/subvol1/.borgmatic-1234',
+        '/mnt/subvol1/.borgmatic-snapshot-1234',
     ).once()
     ).once()
     flexmock(module.shutil).should_receive('rmtree').with_args(
     flexmock(module.shutil).should_receive('rmtree').with_args(
-        '/mnt/subvol1/.borgmatic-5678',
+        '/mnt/subvol1/.borgmatic-snapshot-5678',
     ).once()
     ).once()
     flexmock(module.shutil).should_receive('rmtree').with_args(
     flexmock(module.shutil).should_receive('rmtree').with_args(
-        '/mnt/subvol2/.borgmatic-1234',
+        '/mnt/subvol2/.borgmatic-snapshot-1234',
     ).once()
     ).once()
     flexmock(module.shutil).should_receive('rmtree').with_args(
     flexmock(module.shutil).should_receive('rmtree').with_args(
-        '/mnt/subvol2/.borgmatic-5678',
+        '/mnt/subvol2/.borgmatic-snapshot-5678',
     ).never()
     ).never()
 
 
     module.remove_data_source_dumps(
     module.remove_data_source_dumps(
@@ -901,44 +855,50 @@ def test_remove_data_source_dumps_with_dry_run_skips_deletes():
         ),
         ),
     )
     )
     flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
     flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
-        '/mnt/subvol1/.borgmatic-1234/./mnt/subvol1',
+        '/mnt/subvol1/.borgmatic-snapshot-1234/./mnt/subvol1',
     )
     )
     flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
     flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
-        '/mnt/subvol2/.borgmatic-1234/./mnt/subvol2',
+        '/mnt/subvol2/.borgmatic-snapshot-1234/./mnt/subvol2',
     )
     )
     flexmock(module.borgmatic.config.paths).should_receive(
     flexmock(module.borgmatic.config.paths).should_receive(
         'replace_temporary_subdirectory_with_glob',
         'replace_temporary_subdirectory_with_glob',
     ).with_args(
     ).with_args(
-        '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
+        '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
         temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
         temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
     ).and_return('/mnt/subvol1/.borgmatic-*/mnt/subvol1')
     ).and_return('/mnt/subvol1/.borgmatic-*/mnt/subvol1')
     flexmock(module.borgmatic.config.paths).should_receive(
     flexmock(module.borgmatic.config.paths).should_receive(
         'replace_temporary_subdirectory_with_glob',
         'replace_temporary_subdirectory_with_glob',
     ).with_args(
     ).with_args(
-        '/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
+        '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
         temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
         temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
     ).and_return('/mnt/subvol2/.borgmatic-*/mnt/subvol2')
     ).and_return('/mnt/subvol2/.borgmatic-*/mnt/subvol2')
     flexmock(module.glob).should_receive('glob').with_args(
     flexmock(module.glob).should_receive('glob').with_args(
         '/mnt/subvol1/.borgmatic-*/mnt/subvol1',
         '/mnt/subvol1/.borgmatic-*/mnt/subvol1',
     ).and_return(
     ).and_return(
-        ('/mnt/subvol1/.borgmatic-1234/mnt/subvol1', '/mnt/subvol1/.borgmatic-5678/mnt/subvol1'),
+        (
+            '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
+            '/mnt/subvol1/.borgmatic-snapshot-5678/mnt/subvol1',
+        ),
     )
     )
     flexmock(module.glob).should_receive('glob').with_args(
     flexmock(module.glob).should_receive('glob').with_args(
         '/mnt/subvol2/.borgmatic-*/mnt/subvol2',
         '/mnt/subvol2/.borgmatic-*/mnt/subvol2',
     ).and_return(
     ).and_return(
-        ('/mnt/subvol2/.borgmatic-1234/mnt/subvol2', '/mnt/subvol2/.borgmatic-5678/mnt/subvol2'),
+        (
+            '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
+            '/mnt/subvol2/.borgmatic-snapshot-5678/mnt/subvol2',
+        ),
     )
     )
     flexmock(module.os.path).should_receive('isdir').with_args(
     flexmock(module.os.path).should_receive('isdir').with_args(
-        '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
+        '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
     ).and_return(True)
     ).and_return(True)
     flexmock(module.os.path).should_receive('isdir').with_args(
     flexmock(module.os.path).should_receive('isdir').with_args(
-        '/mnt/subvol1/.borgmatic-5678/mnt/subvol1',
+        '/mnt/subvol1/.borgmatic-snapshot-5678/mnt/subvol1',
     ).and_return(True)
     ).and_return(True)
     flexmock(module.os.path).should_receive('isdir').with_args(
     flexmock(module.os.path).should_receive('isdir').with_args(
-        '/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
+        '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
     ).and_return(True)
     ).and_return(True)
     flexmock(module.os.path).should_receive('isdir').with_args(
     flexmock(module.os.path).should_receive('isdir').with_args(
-        '/mnt/subvol2/.borgmatic-5678/mnt/subvol2',
+        '/mnt/subvol2/.borgmatic-snapshot-5678/mnt/subvol2',
     ).and_return(False)
     ).and_return(False)
     flexmock(module).should_receive('delete_snapshot').never()
     flexmock(module).should_receive('delete_snapshot').never()
     flexmock(module.shutil).should_receive('rmtree').never()
     flexmock(module.shutil).should_receive('rmtree').never()
@@ -980,21 +940,21 @@ def test_remove_data_source_without_snapshots_skips_deletes():
         ),
         ),
     )
     )
     flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
     flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
-        '/mnt/subvol1/.borgmatic-1234/./mnt/subvol1',
+        '/mnt/subvol1/.borgmatic-snapshot-1234/./mnt/subvol1',
     )
     )
     flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
     flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
-        '/mnt/subvol2/.borgmatic-1234/./mnt/subvol2',
+        '/mnt/subvol2/.borgmatic-snapshot-1234/./mnt/subvol2',
     )
     )
     flexmock(module.borgmatic.config.paths).should_receive(
     flexmock(module.borgmatic.config.paths).should_receive(
         'replace_temporary_subdirectory_with_glob',
         'replace_temporary_subdirectory_with_glob',
     ).with_args(
     ).with_args(
-        '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
+        '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
         temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
         temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
     ).and_return('/mnt/subvol1/.borgmatic-*/mnt/subvol1')
     ).and_return('/mnt/subvol1/.borgmatic-*/mnt/subvol1')
     flexmock(module.borgmatic.config.paths).should_receive(
     flexmock(module.borgmatic.config.paths).should_receive(
         'replace_temporary_subdirectory_with_glob',
         'replace_temporary_subdirectory_with_glob',
     ).with_args(
     ).with_args(
-        '/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
+        '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
         temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
         temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
     ).and_return('/mnt/subvol2/.borgmatic-*/mnt/subvol2')
     ).and_return('/mnt/subvol2/.borgmatic-*/mnt/subvol2')
     flexmock(module.glob).should_receive('glob').and_return(())
     flexmock(module.glob).should_receive('glob').and_return(())
@@ -1020,44 +980,50 @@ def test_remove_data_source_dumps_with_delete_snapshot_file_not_found_error_bail
         ),
         ),
     )
     )
     flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
     flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
-        '/mnt/subvol1/.borgmatic-1234/./mnt/subvol1',
+        '/mnt/subvol1/.borgmatic-snapshot-1234/./mnt/subvol1',
     )
     )
     flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
     flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
-        '/mnt/subvol2/.borgmatic-1234/./mnt/subvol2',
+        '/mnt/subvol2/.borgmatic-snapshot-1234/./mnt/subvol2',
     )
     )
     flexmock(module.borgmatic.config.paths).should_receive(
     flexmock(module.borgmatic.config.paths).should_receive(
         'replace_temporary_subdirectory_with_glob',
         'replace_temporary_subdirectory_with_glob',
     ).with_args(
     ).with_args(
-        '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
+        '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
         temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
         temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
     ).and_return('/mnt/subvol1/.borgmatic-*/mnt/subvol1')
     ).and_return('/mnt/subvol1/.borgmatic-*/mnt/subvol1')
     flexmock(module.borgmatic.config.paths).should_receive(
     flexmock(module.borgmatic.config.paths).should_receive(
         'replace_temporary_subdirectory_with_glob',
         'replace_temporary_subdirectory_with_glob',
     ).with_args(
     ).with_args(
-        '/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
+        '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
         temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
         temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
     ).and_return('/mnt/subvol2/.borgmatic-*/mnt/subvol2')
     ).and_return('/mnt/subvol2/.borgmatic-*/mnt/subvol2')
     flexmock(module.glob).should_receive('glob').with_args(
     flexmock(module.glob).should_receive('glob').with_args(
         '/mnt/subvol1/.borgmatic-*/mnt/subvol1',
         '/mnt/subvol1/.borgmatic-*/mnt/subvol1',
     ).and_return(
     ).and_return(
-        ('/mnt/subvol1/.borgmatic-1234/mnt/subvol1', '/mnt/subvol1/.borgmatic-5678/mnt/subvol1'),
+        (
+            '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
+            '/mnt/subvol1/.borgmatic-snapshot-5678/mnt/subvol1',
+        ),
     )
     )
     flexmock(module.glob).should_receive('glob').with_args(
     flexmock(module.glob).should_receive('glob').with_args(
         '/mnt/subvol2/.borgmatic-*/mnt/subvol2',
         '/mnt/subvol2/.borgmatic-*/mnt/subvol2',
     ).and_return(
     ).and_return(
-        ('/mnt/subvol2/.borgmatic-1234/mnt/subvol2', '/mnt/subvol2/.borgmatic-5678/mnt/subvol2'),
+        (
+            '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
+            '/mnt/subvol2/.borgmatic-snapshot-5678/mnt/subvol2',
+        ),
     )
     )
     flexmock(module.os.path).should_receive('isdir').with_args(
     flexmock(module.os.path).should_receive('isdir').with_args(
-        '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
+        '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
     ).and_return(True)
     ).and_return(True)
     flexmock(module.os.path).should_receive('isdir').with_args(
     flexmock(module.os.path).should_receive('isdir').with_args(
-        '/mnt/subvol1/.borgmatic-5678/mnt/subvol1',
+        '/mnt/subvol1/.borgmatic-snapshot-5678/mnt/subvol1',
     ).and_return(True)
     ).and_return(True)
     flexmock(module.os.path).should_receive('isdir').with_args(
     flexmock(module.os.path).should_receive('isdir').with_args(
-        '/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
+        '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
     ).and_return(True)
     ).and_return(True)
     flexmock(module.os.path).should_receive('isdir').with_args(
     flexmock(module.os.path).should_receive('isdir').with_args(
-        '/mnt/subvol2/.borgmatic-5678/mnt/subvol2',
+        '/mnt/subvol2/.borgmatic-snapshot-5678/mnt/subvol2',
     ).and_return(False)
     ).and_return(False)
     flexmock(module).should_receive('delete_snapshot').and_raise(FileNotFoundError)
     flexmock(module).should_receive('delete_snapshot').and_raise(FileNotFoundError)
     flexmock(module.shutil).should_receive('rmtree').never()
     flexmock(module.shutil).should_receive('rmtree').never()
@@ -1080,44 +1046,50 @@ def test_remove_data_source_dumps_with_delete_snapshot_called_process_error_bail
         ),
         ),
     )
     )
     flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
     flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
-        '/mnt/subvol1/.borgmatic-1234/./mnt/subvol1',
+        '/mnt/subvol1/.borgmatic-snapshot-1234/./mnt/subvol1',
     )
     )
     flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
     flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
-        '/mnt/subvol2/.borgmatic-1234/./mnt/subvol2',
+        '/mnt/subvol2/.borgmatic-snapshot-1234/./mnt/subvol2',
     )
     )
     flexmock(module.borgmatic.config.paths).should_receive(
     flexmock(module.borgmatic.config.paths).should_receive(
         'replace_temporary_subdirectory_with_glob',
         'replace_temporary_subdirectory_with_glob',
     ).with_args(
     ).with_args(
-        '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
+        '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
         temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
         temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
     ).and_return('/mnt/subvol1/.borgmatic-*/mnt/subvol1')
     ).and_return('/mnt/subvol1/.borgmatic-*/mnt/subvol1')
     flexmock(module.borgmatic.config.paths).should_receive(
     flexmock(module.borgmatic.config.paths).should_receive(
         'replace_temporary_subdirectory_with_glob',
         'replace_temporary_subdirectory_with_glob',
     ).with_args(
     ).with_args(
-        '/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
+        '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
         temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
         temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
     ).and_return('/mnt/subvol2/.borgmatic-*/mnt/subvol2')
     ).and_return('/mnt/subvol2/.borgmatic-*/mnt/subvol2')
     flexmock(module.glob).should_receive('glob').with_args(
     flexmock(module.glob).should_receive('glob').with_args(
         '/mnt/subvol1/.borgmatic-*/mnt/subvol1',
         '/mnt/subvol1/.borgmatic-*/mnt/subvol1',
     ).and_return(
     ).and_return(
-        ('/mnt/subvol1/.borgmatic-1234/mnt/subvol1', '/mnt/subvol1/.borgmatic-5678/mnt/subvol1'),
+        (
+            '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
+            '/mnt/subvol1/.borgmatic-snapshot-5678/mnt/subvol1',
+        ),
     )
     )
     flexmock(module.glob).should_receive('glob').with_args(
     flexmock(module.glob).should_receive('glob').with_args(
         '/mnt/subvol2/.borgmatic-*/mnt/subvol2',
         '/mnt/subvol2/.borgmatic-*/mnt/subvol2',
     ).and_return(
     ).and_return(
-        ('/mnt/subvol2/.borgmatic-1234/mnt/subvol2', '/mnt/subvol2/.borgmatic-5678/mnt/subvol2'),
+        (
+            '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
+            '/mnt/subvol2/.borgmatic-snapshot-5678/mnt/subvol2',
+        ),
     )
     )
     flexmock(module.os.path).should_receive('isdir').with_args(
     flexmock(module.os.path).should_receive('isdir').with_args(
-        '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
+        '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
     ).and_return(True)
     ).and_return(True)
     flexmock(module.os.path).should_receive('isdir').with_args(
     flexmock(module.os.path).should_receive('isdir').with_args(
-        '/mnt/subvol1/.borgmatic-5678/mnt/subvol1',
+        '/mnt/subvol1/.borgmatic-snapshot-5678/mnt/subvol1',
     ).and_return(True)
     ).and_return(True)
     flexmock(module.os.path).should_receive('isdir').with_args(
     flexmock(module.os.path).should_receive('isdir').with_args(
-        '/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
+        '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
     ).and_return(True)
     ).and_return(True)
     flexmock(module.os.path).should_receive('isdir').with_args(
     flexmock(module.os.path).should_receive('isdir').with_args(
-        '/mnt/subvol2/.borgmatic-5678/mnt/subvol2',
+        '/mnt/subvol2/.borgmatic-snapshot-5678/mnt/subvol2',
     ).and_return(False)
     ).and_return(False)
     flexmock(module).should_receive('delete_snapshot').and_raise(
     flexmock(module).should_receive('delete_snapshot').and_raise(
         module.subprocess.CalledProcessError(1, 'command', 'error'),
         module.subprocess.CalledProcessError(1, 'command', 'error'),
@@ -1140,29 +1112,37 @@ def test_remove_data_source_dumps_with_root_subvolume_skips_duplicate_removal():
     )
     )
 
 
     flexmock(module).should_receive('make_snapshot_path').with_args('/').and_return(
     flexmock(module).should_receive('make_snapshot_path').with_args('/').and_return(
-        '/.borgmatic-1234',
+        '/.borgmatic-snapshot-1234',
     )
     )
 
 
     flexmock(module.borgmatic.config.paths).should_receive(
     flexmock(module.borgmatic.config.paths).should_receive(
         'replace_temporary_subdirectory_with_glob',
         'replace_temporary_subdirectory_with_glob',
     ).with_args(
     ).with_args(
-        '/.borgmatic-1234',
+        '/.borgmatic-snapshot-1234',
         temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
         temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
     ).and_return('/.borgmatic-*')
     ).and_return('/.borgmatic-*')
 
 
     flexmock(module.glob).should_receive('glob').with_args('/.borgmatic-*').and_return(
     flexmock(module.glob).should_receive('glob').with_args('/.borgmatic-*').and_return(
-        ('/.borgmatic-1234', '/.borgmatic-5678'),
+        ('/.borgmatic-snapshot-1234', '/.borgmatic-snapshot-5678'),
     )
     )
 
 
-    flexmock(module.os.path).should_receive('isdir').with_args('/.borgmatic-1234').and_return(
+    flexmock(module.os.path).should_receive('isdir').with_args(
+        '/.borgmatic-snapshot-1234'
+    ).and_return(
         True,
         True,
     ).and_return(False)
     ).and_return(False)
-    flexmock(module.os.path).should_receive('isdir').with_args('/.borgmatic-5678').and_return(
+    flexmock(module.os.path).should_receive('isdir').with_args(
+        '/.borgmatic-snapshot-5678'
+    ).and_return(
         True,
         True,
     ).and_return(False)
     ).and_return(False)
 
 
-    flexmock(module).should_receive('delete_snapshot').with_args('btrfs', '/.borgmatic-1234').once()
-    flexmock(module).should_receive('delete_snapshot').with_args('btrfs', '/.borgmatic-5678').once()
+    flexmock(module).should_receive('delete_snapshot').with_args(
+        'btrfs', '/.borgmatic-snapshot-1234'
+    ).once()
+    flexmock(module).should_receive('delete_snapshot').with_args(
+        'btrfs', '/.borgmatic-snapshot-5678'
+    ).once()
 
 
     flexmock(module.os.path).should_receive('isdir').with_args('').and_return(False)
     flexmock(module.os.path).should_receive('isdir').with_args('').and_return(False)
 
 

+ 113 - 0
tests/unit/hooks/data_source/test_config.py

@@ -155,3 +155,116 @@ def test_get_ip_from_container_with_broken_output_errors():
         module.get_ip_from_container('yolo')
         module.get_ip_from_container('yolo')
 
 
     assert 'Could not decode JSON output' in str(exc_info.value)
     assert 'Could not decode JSON output' in str(exc_info.value)
+
+
+def test_inject_pattern_prepends_pattern_in_list():
+    patterns = [
+        module.borgmatic.borg.pattern.Pattern('/etc'),
+        module.borgmatic.borg.pattern.Pattern('/var'),
+    ]
+
+    module.inject_pattern(
+        patterns,
+        module.borgmatic.borg.pattern.Pattern(
+            '/foo/bar',
+            type=module.borgmatic.borg.pattern.Pattern_type.EXCLUDE,
+        ),
+    )
+
+    assert patterns == [
+        module.borgmatic.borg.pattern.Pattern(
+            '/foo/bar',
+            type=module.borgmatic.borg.pattern.Pattern_type.EXCLUDE,
+        ),
+        module.borgmatic.borg.pattern.Pattern('/etc'),
+        module.borgmatic.borg.pattern.Pattern('/var'),
+    ]
+
+
+def test_inject_pattern_with_root_pattern_prepends_it_along_with_corresponding_include_pattern():
+    patterns = [
+        module.borgmatic.borg.pattern.Pattern('/etc'),
+        module.borgmatic.borg.pattern.Pattern('/var'),
+    ]
+
+    module.inject_pattern(
+        patterns,
+        module.borgmatic.borg.pattern.Pattern('/foo/bar'),
+    )
+
+    assert patterns == [
+        module.borgmatic.borg.pattern.Pattern('/foo/bar'),
+        module.borgmatic.borg.pattern.Pattern(
+            '/foo/bar',
+            type=module.borgmatic.borg.pattern.Pattern_type.INCLUDE,
+        ),
+        module.borgmatic.borg.pattern.Pattern('/etc'),
+        module.borgmatic.borg.pattern.Pattern('/var'),
+    ]
+
+
+def test_replace_pattern_swaps_out_pattern_in_place():
+    patterns = [
+        module.borgmatic.borg.pattern.Pattern('/etc'),
+        module.borgmatic.borg.pattern.Pattern('/var'),
+        module.borgmatic.borg.pattern.Pattern('/lib'),
+    ]
+
+    module.replace_pattern(
+        patterns,
+        module.borgmatic.borg.pattern.Pattern('/var'),
+        module.borgmatic.borg.pattern.Pattern(
+            '/foo/bar',
+            type=module.borgmatic.borg.pattern.Pattern_type.EXCLUDE,
+        ),
+    )
+
+    assert patterns == [
+        module.borgmatic.borg.pattern.Pattern('/etc'),
+        module.borgmatic.borg.pattern.Pattern(
+            '/foo/bar',
+            type=module.borgmatic.borg.pattern.Pattern_type.EXCLUDE,
+        ),
+        module.borgmatic.borg.pattern.Pattern('/lib'),
+    ]
+
+
+def test_replace_pattern_with_unknown_pattern_falls_back_to_injecting():
+    patterns = [
+        module.borgmatic.borg.pattern.Pattern('/etc'),
+        module.borgmatic.borg.pattern.Pattern('/var'),
+        module.borgmatic.borg.pattern.Pattern('/lib'),
+    ]
+    flexmock(module).should_receive('inject_pattern').with_args(
+        patterns, module.borgmatic.borg.pattern.Pattern('/foo/bar')
+    ).once()
+
+    module.replace_pattern(
+        patterns,
+        module.borgmatic.borg.pattern.Pattern('/unknown'),
+        module.borgmatic.borg.pattern.Pattern('/foo/bar'),
+    )
+
+
+def test_replace_pattern_with_root_pattern_swaps_it_in_along_with_corresponding_include_pattern():
+    patterns = [
+        module.borgmatic.borg.pattern.Pattern('/etc'),
+        module.borgmatic.borg.pattern.Pattern('/var'),
+        module.borgmatic.borg.pattern.Pattern('/lib'),
+    ]
+
+    module.replace_pattern(
+        patterns,
+        module.borgmatic.borg.pattern.Pattern('/var'),
+        module.borgmatic.borg.pattern.Pattern('/foo/bar'),
+    )
+
+    assert patterns == [
+        module.borgmatic.borg.pattern.Pattern('/etc'),
+        module.borgmatic.borg.pattern.Pattern('/foo/bar'),
+        module.borgmatic.borg.pattern.Pattern(
+            '/foo/bar',
+            type=module.borgmatic.borg.pattern.Pattern_type.INCLUDE,
+        ),
+        module.borgmatic.borg.pattern.Pattern('/lib'),
+    ]

+ 68 - 29
tests/unit/hooks/data_source/test_lvm.py

@@ -294,7 +294,7 @@ def test_make_borg_snapshot_pattern_includes_slashdot_hack_and_stripped_pattern_
     )
     )
 
 
 
 
-def test_dump_data_sources_snapshots_and_mounts_and_updates_patterns():
+def test_dump_data_sources_snapshots_and_mounts_and_replaces_patterns():
     config = {'lvm': {}}
     config = {'lvm': {}}
     patterns = [Pattern('/mnt/lvolume1/subdir'), Pattern('/mnt/lvolume2')]
     patterns = [Pattern('/mnt/lvolume1/subdir'), Pattern('/mnt/lvolume2')]
     logical_volumes = (
     logical_volumes = (
@@ -360,6 +360,22 @@ def test_dump_data_sources_snapshots_and_mounts_and_updates_patterns():
         logical_volumes[1],
         logical_volumes[1],
         '/run/borgmatic',
         '/run/borgmatic',
     ).and_return(Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2'))
     ).and_return(Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2'))
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
+        object,
+        Pattern('/mnt/lvolume1/subdir'),
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume1/subdir',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
+        object,
+        Pattern('/mnt/lvolume2'),
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
 
 
     assert (
     assert (
         module.dump_data_sources(
         module.dump_data_sources(
@@ -373,11 +389,6 @@ def test_dump_data_sources_snapshots_and_mounts_and_updates_patterns():
         == []
         == []
     )
     )
 
 
-    assert patterns == [
-        Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume1/subdir'),
-        Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2'),
-    ]
-
 
 
 def test_dump_data_sources_with_no_logical_volumes_skips_snapshots():
 def test_dump_data_sources_with_no_logical_volumes_skips_snapshots():
     config = {'lvm': {}}
     config = {'lvm': {}}
@@ -385,6 +396,7 @@ def test_dump_data_sources_with_no_logical_volumes_skips_snapshots():
     flexmock(module).should_receive('get_logical_volumes').and_return(())
     flexmock(module).should_receive('get_logical_volumes').and_return(())
     flexmock(module).should_receive('snapshot_logical_volume').never()
     flexmock(module).should_receive('snapshot_logical_volume').never()
     flexmock(module).should_receive('mount_snapshot').never()
     flexmock(module).should_receive('mount_snapshot').never()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').never()
 
 
     assert (
     assert (
         module.dump_data_sources(
         module.dump_data_sources(
@@ -398,8 +410,6 @@ def test_dump_data_sources_with_no_logical_volumes_skips_snapshots():
         == []
         == []
     )
     )
 
 
-    assert patterns == [Pattern('/mnt/lvolume1/subdir'), Pattern('/mnt/lvolume2')]
-
 
 
 def test_dump_data_sources_uses_snapshot_size_for_snapshot():
 def test_dump_data_sources_uses_snapshot_size_for_snapshot():
     config = {'lvm': {'snapshot_size': '1000PB'}}
     config = {'lvm': {'snapshot_size': '1000PB'}}
@@ -467,6 +477,22 @@ def test_dump_data_sources_uses_snapshot_size_for_snapshot():
         logical_volumes[1],
         logical_volumes[1],
         '/run/borgmatic',
         '/run/borgmatic',
     ).and_return(Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2'))
     ).and_return(Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2'))
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
+        object,
+        Pattern('/mnt/lvolume1/subdir'),
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume1/subdir',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
+        object,
+        Pattern('/mnt/lvolume2'),
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
 
 
     assert (
     assert (
         module.dump_data_sources(
         module.dump_data_sources(
@@ -480,11 +506,6 @@ def test_dump_data_sources_uses_snapshot_size_for_snapshot():
         == []
         == []
     )
     )
 
 
-    assert patterns == [
-        Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume1/subdir'),
-        Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2'),
-    ]
-
 
 
 def test_dump_data_sources_uses_custom_commands():
 def test_dump_data_sources_uses_custom_commands():
     config = {
     config = {
@@ -559,6 +580,22 @@ def test_dump_data_sources_uses_custom_commands():
         logical_volumes[1],
         logical_volumes[1],
         '/run/borgmatic',
         '/run/borgmatic',
     ).and_return(Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2'))
     ).and_return(Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2'))
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
+        object,
+        Pattern('/mnt/lvolume1/subdir'),
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume1/subdir',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
+        object,
+        Pattern('/mnt/lvolume2'),
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
 
 
     assert (
     assert (
         module.dump_data_sources(
         module.dump_data_sources(
@@ -572,11 +609,6 @@ def test_dump_data_sources_uses_custom_commands():
         == []
         == []
     )
     )
 
 
-    assert patterns == [
-        Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume1/subdir'),
-        Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2'),
-    ]
-
 
 
 def test_dump_data_sources_with_dry_run_skips_snapshots_and_does_not_touch_patterns():
 def test_dump_data_sources_with_dry_run_skips_snapshots_and_does_not_touch_patterns():
     config = {'lvm': {}}
     config = {'lvm': {}}
@@ -601,6 +633,7 @@ def test_dump_data_sources_with_dry_run_skips_snapshots_and_does_not_touch_patte
     flexmock(module).should_receive('snapshot_logical_volume').never()
     flexmock(module).should_receive('snapshot_logical_volume').never()
     flexmock(module).should_receive('get_snapshots').never()
     flexmock(module).should_receive('get_snapshots').never()
     flexmock(module).should_receive('mount_snapshot').never()
     flexmock(module).should_receive('mount_snapshot').never()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').never()
 
 
     assert (
     assert (
         module.dump_data_sources(
         module.dump_data_sources(
@@ -614,11 +647,6 @@ def test_dump_data_sources_with_dry_run_skips_snapshots_and_does_not_touch_patte
         == []
         == []
     )
     )
 
 
-    assert patterns == [
-        Pattern('/mnt/lvolume1/subdir'),
-        Pattern('/mnt/lvolume2'),
-    ]
-
 
 
 def test_dump_data_sources_ignores_mismatch_between_given_patterns_and_contained_patterns():
 def test_dump_data_sources_ignores_mismatch_between_given_patterns_and_contained_patterns():
     config = {'lvm': {}}
     config = {'lvm': {}}
@@ -686,6 +714,22 @@ def test_dump_data_sources_ignores_mismatch_between_given_patterns_and_contained
         logical_volumes[1],
         logical_volumes[1],
         '/run/borgmatic',
         '/run/borgmatic',
     ).and_return(Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2'))
     ).and_return(Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2'))
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
+        object,
+        Pattern('/mnt/lvolume1/subdir'),
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume1/subdir',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
+        object,
+        Pattern('/mnt/lvolume2'),
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
 
 
     assert (
     assert (
         module.dump_data_sources(
         module.dump_data_sources(
@@ -699,12 +743,6 @@ def test_dump_data_sources_ignores_mismatch_between_given_patterns_and_contained
         == []
         == []
     )
     )
 
 
-    assert patterns == [
-        Pattern('/hmm'),
-        Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume1/subdir'),
-        Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2'),
-    ]
-
 
 
 def test_dump_data_sources_with_missing_snapshot_errors():
 def test_dump_data_sources_with_missing_snapshot_errors():
     config = {'lvm': {}}
     config = {'lvm': {}}
@@ -747,6 +785,7 @@ def test_dump_data_sources_with_missing_snapshot_errors():
         snapshot_name='lvolume2_borgmatic-1234',
         snapshot_name='lvolume2_borgmatic-1234',
     ).never()
     ).never()
     flexmock(module).should_receive('mount_snapshot').never()
     flexmock(module).should_receive('mount_snapshot').never()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').never()
 
 
     with pytest.raises(ValueError):
     with pytest.raises(ValueError):
         module.dump_data_sources(
         module.dump_data_sources(

+ 37 - 0
tests/unit/hooks/data_source/test_mariadb.py

@@ -409,6 +409,13 @@ def test_dump_data_sources_dumps_each_database():
             module.borgmatic.actions.restore.Dump('mariadb_databases', 'bar'),
             module.borgmatic.actions.restore.Dump('mariadb_databases', 'bar'),
         ],
         ],
     ).once()
     ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/mariadb_databases',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
 
 
     assert (
     assert (
         module.dump_data_sources(
         module.dump_data_sources(
@@ -458,6 +465,13 @@ def test_dump_data_sources_dumps_with_password():
             module.borgmatic.actions.restore.Dump('mariadb_databases', 'foo'),
             module.borgmatic.actions.restore.Dump('mariadb_databases', 'foo'),
         ],
         ],
     ).once()
     ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/mariadb_databases',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
 
 
     assert module.dump_data_sources(
     assert module.dump_data_sources(
         [database],
         [database],
@@ -509,6 +523,13 @@ def test_dump_data_sources_dumps_with_environment_password_transport_passes_pass
             module.borgmatic.actions.restore.Dump('mariadb_databases', 'foo'),
             module.borgmatic.actions.restore.Dump('mariadb_databases', 'foo'),
         ],
         ],
     ).once()
     ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/mariadb_databases',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
 
 
     assert module.dump_data_sources(
     assert module.dump_data_sources(
         [database],
         [database],
@@ -547,6 +568,13 @@ def test_dump_data_sources_dumps_all_databases_at_once():
             module.borgmatic.actions.restore.Dump('mariadb_databases', 'all'),
             module.borgmatic.actions.restore.Dump('mariadb_databases', 'all'),
         ],
         ],
     ).once()
     ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/mariadb_databases',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
 
 
     assert module.dump_data_sources(
     assert module.dump_data_sources(
         databases,
         databases,
@@ -589,6 +617,13 @@ def test_dump_data_sources_dumps_all_databases_separately_when_format_configured
             module.borgmatic.actions.restore.Dump('mariadb_databases', 'bar'),
             module.borgmatic.actions.restore.Dump('mariadb_databases', 'bar'),
         ],
         ],
     ).once()
     ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/mariadb_databases',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
 
 
     assert (
     assert (
         module.dump_data_sources(
         module.dump_data_sources(
@@ -614,6 +649,7 @@ def test_dump_data_sources_errors_for_missing_all_databases():
         'databases/localhost/all',
         'databases/localhost/all',
     )
     )
     flexmock(module).should_receive('database_names_to_dump').and_return(())
     flexmock(module).should_receive('database_names_to_dump').and_return(())
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').never()
 
 
     with pytest.raises(ValueError):
     with pytest.raises(ValueError):
         assert module.dump_data_sources(
         assert module.dump_data_sources(
@@ -637,6 +673,7 @@ def test_dump_data_sources_does_not_error_for_missing_all_databases_with_dry_run
         'databases/localhost/all',
         'databases/localhost/all',
     )
     )
     flexmock(module).should_receive('database_names_to_dump').and_return(())
     flexmock(module).should_receive('database_names_to_dump').and_return(())
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').never()
 
 
     assert (
     assert (
         module.dump_data_sources(
         module.dump_data_sources(

+ 43 - 0
tests/unit/hooks/data_source/test_mongodb.py

@@ -47,6 +47,13 @@ def test_dump_data_sources_runs_mongodump_for_each_database():
             module.borgmatic.actions.restore.Dump('mongodb_databases', 'bar'),
             module.borgmatic.actions.restore.Dump('mongodb_databases', 'bar'),
         ],
         ],
     ).once()
     ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/mongodb_databases',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
 
 
     assert (
     assert (
         module.dump_data_sources(
         module.dump_data_sources(
@@ -70,6 +77,7 @@ def test_dump_data_sources_with_dry_run_skips_mongodump():
     flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
     flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
     flexmock(module).should_receive('execute_command').never()
     flexmock(module).should_receive('execute_command').never()
     flexmock(module.dump).should_receive('write_data_source_dumps_metadata').never()
     flexmock(module.dump).should_receive('write_data_source_dumps_metadata').never()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').never()
 
 
     assert (
     assert (
         module.dump_data_sources(
         module.dump_data_sources(
@@ -118,6 +126,13 @@ def test_dump_data_sources_runs_mongodump_with_hostname_and_port():
             ),
             ),
         ],
         ],
     ).once()
     ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/mongodb_databases',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
 
 
     assert module.dump_data_sources(
     assert module.dump_data_sources(
         databases,
         databases,
@@ -176,6 +191,13 @@ def test_dump_data_sources_runs_mongodump_with_username_and_password():
             module.borgmatic.actions.restore.Dump('mongodb_databases', 'foo'),
             module.borgmatic.actions.restore.Dump('mongodb_databases', 'foo'),
         ],
         ],
     ).once()
     ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/mongodb_databases',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
 
 
     assert module.dump_data_sources(
     assert module.dump_data_sources(
         databases,
         databases,
@@ -207,6 +229,13 @@ def test_dump_data_sources_runs_mongodump_with_directory_format():
             module.borgmatic.actions.restore.Dump('mongodb_databases', 'foo'),
             module.borgmatic.actions.restore.Dump('mongodb_databases', 'foo'),
         ],
         ],
     ).once()
     ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/mongodb_databases',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
 
 
     assert (
     assert (
         module.dump_data_sources(
         module.dump_data_sources(
@@ -250,6 +279,13 @@ def test_dump_data_sources_runs_mongodump_with_options():
             module.borgmatic.actions.restore.Dump('mongodb_databases', 'foo'),
             module.borgmatic.actions.restore.Dump('mongodb_databases', 'foo'),
         ],
         ],
     ).once()
     ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/mongodb_databases',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
 
 
     assert module.dump_data_sources(
     assert module.dump_data_sources(
         databases,
         databases,
@@ -282,6 +318,13 @@ def test_dump_data_sources_runs_mongodumpall_for_all_databases():
             module.borgmatic.actions.restore.Dump('mongodb_databases', 'all'),
             module.borgmatic.actions.restore.Dump('mongodb_databases', 'all'),
         ],
         ],
     ).once()
     ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/mongodb_databases',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
 
 
     assert module.dump_data_sources(
     assert module.dump_data_sources(
         databases,
         databases,

+ 37 - 0
tests/unit/hooks/data_source/test_mysql.py

@@ -296,6 +296,13 @@ def test_dump_data_sources_dumps_each_database():
             module.borgmatic.actions.restore.Dump('mysql_databases', 'bar'),
             module.borgmatic.actions.restore.Dump('mysql_databases', 'bar'),
         ],
         ],
     ).once()
     ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/mysql_databases',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
 
 
     assert (
     assert (
         module.dump_data_sources(
         module.dump_data_sources(
@@ -345,6 +352,13 @@ def test_dump_data_sources_dumps_with_password():
             module.borgmatic.actions.restore.Dump('mysql_databases', 'foo'),
             module.borgmatic.actions.restore.Dump('mysql_databases', 'foo'),
         ],
         ],
     ).once()
     ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/mysql_databases',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
 
 
     assert module.dump_data_sources(
     assert module.dump_data_sources(
         [database],
         [database],
@@ -396,6 +410,13 @@ def test_dump_data_sources_dumps_with_environment_password_transport_passes_pass
             module.borgmatic.actions.restore.Dump('mysql_databases', 'foo'),
             module.borgmatic.actions.restore.Dump('mysql_databases', 'foo'),
         ],
         ],
     ).once()
     ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/mysql_databases',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
 
 
     assert module.dump_data_sources(
     assert module.dump_data_sources(
         [database],
         [database],
@@ -435,6 +456,13 @@ def test_dump_data_sources_dumps_all_databases_at_once():
             module.borgmatic.actions.restore.Dump('mysql_databases', 'all'),
             module.borgmatic.actions.restore.Dump('mysql_databases', 'all'),
         ],
         ],
     ).once()
     ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/mysql_databases',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
 
 
     assert module.dump_data_sources(
     assert module.dump_data_sources(
         databases,
         databases,
@@ -477,6 +505,13 @@ def test_dump_data_sources_dumps_all_databases_separately_when_format_configured
             module.borgmatic.actions.restore.Dump('mysql_databases', 'bar'),
             module.borgmatic.actions.restore.Dump('mysql_databases', 'bar'),
         ],
         ],
     ).once()
     ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/mysql_databases',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
 
 
     assert (
     assert (
         module.dump_data_sources(
         module.dump_data_sources(
@@ -502,6 +537,7 @@ def test_dump_data_sources_errors_for_missing_all_databases():
         'databases/localhost/all',
         'databases/localhost/all',
     )
     )
     flexmock(module).should_receive('database_names_to_dump').and_return(())
     flexmock(module).should_receive('database_names_to_dump').and_return(())
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').never()
 
 
     with pytest.raises(ValueError):
     with pytest.raises(ValueError):
         assert module.dump_data_sources(
         assert module.dump_data_sources(
@@ -525,6 +561,7 @@ def test_dump_data_sources_does_not_error_for_missing_all_databases_with_dry_run
         'databases/localhost/all',
         'databases/localhost/all',
     )
     )
     flexmock(module).should_receive('database_names_to_dump').and_return(())
     flexmock(module).should_receive('database_names_to_dump').and_return(())
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').never()
 
 
     assert (
     assert (
         module.dump_data_sources(
         module.dump_data_sources(

+ 80 - 0
tests/unit/hooks/data_source/test_postgresql.py

@@ -281,6 +281,13 @@ def test_dump_data_sources_runs_pg_dump_for_each_database():
             module.borgmatic.actions.restore.Dump('postgresql_databases', 'bar'),
             module.borgmatic.actions.restore.Dump('postgresql_databases', 'bar'),
         ],
         ],
     ).once()
     ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/postgresql_databases',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
 
 
     assert (
     assert (
         module.dump_data_sources(
         module.dump_data_sources(
@@ -300,6 +307,7 @@ def test_dump_data_sources_raises_when_no_database_names_to_dump():
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module).should_receive('database_names_to_dump').and_return(())
     flexmock(module).should_receive('database_names_to_dump').and_return(())
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').never()
 
 
     with pytest.raises(ValueError):
     with pytest.raises(ValueError):
         module.dump_data_sources(
         module.dump_data_sources(
@@ -317,6 +325,7 @@ def test_dump_data_sources_does_not_raise_when_no_database_names_to_dump():
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
     flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module).should_receive('database_names_to_dump').and_return(())
     flexmock(module).should_receive('database_names_to_dump').and_return(())
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').never()
 
 
     assert (
     assert (
         module.dump_data_sources(
         module.dump_data_sources(
@@ -352,6 +361,13 @@ def test_dump_data_sources_with_duplicate_dump_skips_pg_dump():
             module.borgmatic.actions.restore.Dump('postgresql_databases', 'bar'),
             module.borgmatic.actions.restore.Dump('postgresql_databases', 'bar'),
         ],
         ],
     ).once()
     ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/postgresql_databases',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
 
 
     assert (
     assert (
         module.dump_data_sources(
         module.dump_data_sources(
@@ -383,6 +399,7 @@ def test_dump_data_sources_with_dry_run_skips_pg_dump():
     flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
     flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
     flexmock(module).should_receive('execute_command').never()
     flexmock(module).should_receive('execute_command').never()
     flexmock(module.dump).should_receive('write_data_source_dumps_metadata').never()
     flexmock(module.dump).should_receive('write_data_source_dumps_metadata').never()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').never()
 
 
     assert (
     assert (
         module.dump_data_sources(
         module.dump_data_sources(
@@ -441,6 +458,13 @@ def test_dump_data_sources_runs_pg_dump_with_hostname_and_port():
             ),
             ),
         ],
         ],
     ).once()
     ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/postgresql_databases',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
 
 
     assert module.dump_data_sources(
     assert module.dump_data_sources(
         databases,
         databases,
@@ -494,6 +518,13 @@ def test_dump_data_sources_runs_pg_dump_with_username_and_password():
             module.borgmatic.actions.restore.Dump('postgresql_databases', 'foo'),
             module.borgmatic.actions.restore.Dump('postgresql_databases', 'foo'),
         ],
         ],
     ).once()
     ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/postgresql_databases',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
 
 
     assert module.dump_data_sources(
     assert module.dump_data_sources(
         databases,
         databases,
@@ -547,6 +578,13 @@ def test_dump_data_sources_with_username_injection_attack_gets_escaped():
             module.borgmatic.actions.restore.Dump('postgresql_databases', 'foo'),
             module.borgmatic.actions.restore.Dump('postgresql_databases', 'foo'),
         ],
         ],
     ).once()
     ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/postgresql_databases',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
 
 
     assert module.dump_data_sources(
     assert module.dump_data_sources(
         databases,
         databases,
@@ -595,6 +633,13 @@ def test_dump_data_sources_runs_pg_dump_with_directory_format():
             module.borgmatic.actions.restore.Dump('postgresql_databases', 'foo'),
             module.borgmatic.actions.restore.Dump('postgresql_databases', 'foo'),
         ],
         ],
     ).once()
     ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/postgresql_databases',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
 
 
     assert (
     assert (
         module.dump_data_sources(
         module.dump_data_sources(
@@ -649,6 +694,13 @@ def test_dump_data_sources_runs_pg_dump_with_string_compression():
             module.borgmatic.actions.restore.Dump('postgresql_databases', 'foo'),
             module.borgmatic.actions.restore.Dump('postgresql_databases', 'foo'),
         ],
         ],
     ).once()
     ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/postgresql_databases',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
 
 
     assert (
     assert (
         module.dump_data_sources(
         module.dump_data_sources(
@@ -703,6 +755,13 @@ def test_dump_data_sources_runs_pg_dump_with_integer_compression():
             module.borgmatic.actions.restore.Dump('postgresql_databases', 'foo'),
             module.borgmatic.actions.restore.Dump('postgresql_databases', 'foo'),
         ],
         ],
     ).once()
     ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/postgresql_databases',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
 
 
     assert (
     assert (
         module.dump_data_sources(
         module.dump_data_sources(
@@ -756,6 +815,13 @@ def test_dump_data_sources_runs_pg_dump_with_options():
             module.borgmatic.actions.restore.Dump('postgresql_databases', 'foo'),
             module.borgmatic.actions.restore.Dump('postgresql_databases', 'foo'),
         ],
         ],
     ).once()
     ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/postgresql_databases',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
 
 
     assert module.dump_data_sources(
     assert module.dump_data_sources(
         databases,
         databases,
@@ -795,6 +861,13 @@ def test_dump_data_sources_runs_pg_dumpall_for_all_databases():
             module.borgmatic.actions.restore.Dump('postgresql_databases', 'all'),
             module.borgmatic.actions.restore.Dump('postgresql_databases', 'all'),
         ],
         ],
     ).once()
     ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/postgresql_databases',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
 
 
     assert module.dump_data_sources(
     assert module.dump_data_sources(
         databases,
         databases,
@@ -846,6 +919,13 @@ def test_dump_data_sources_runs_non_default_pg_dump():
             module.borgmatic.actions.restore.Dump('postgresql_databases', 'foo'),
             module.borgmatic.actions.restore.Dump('postgresql_databases', 'foo'),
         ],
         ],
     ).once()
     ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/postgresql_databases',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
 
 
     assert module.dump_data_sources(
     assert module.dump_data_sources(
         databases,
         databases,

+ 43 - 0
tests/unit/hooks/data_source/test_sqlite.py

@@ -33,6 +33,13 @@ def test_dump_data_sources_logs_and_skips_if_dump_already_exists():
             module.borgmatic.actions.restore.Dump('sqlite_databases', 'database'),
             module.borgmatic.actions.restore.Dump('sqlite_databases', 'database'),
         ],
         ],
     ).once()
     ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/sqlite_databases',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
 
 
     assert (
     assert (
         module.dump_data_sources(
         module.dump_data_sources(
@@ -71,6 +78,13 @@ def test_dump_data_sources_dumps_each_database():
             module.borgmatic.actions.restore.Dump('sqlite_databases', 'database2'),
             module.borgmatic.actions.restore.Dump('sqlite_databases', 'database2'),
         ],
         ],
     ).once()
     ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/sqlite_databases',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
 
 
     assert (
     assert (
         module.dump_data_sources(
         module.dump_data_sources(
@@ -115,6 +129,13 @@ def test_dump_data_sources_with_path_injection_attack_gets_escaped():
             module.borgmatic.actions.restore.Dump('sqlite_databases', 'database1'),
             module.borgmatic.actions.restore.Dump('sqlite_databases', 'database1'),
         ],
         ],
     ).once()
     ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/sqlite_databases',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
 
 
     assert (
     assert (
         module.dump_data_sources(
         module.dump_data_sources(
@@ -164,6 +185,13 @@ def test_dump_data_sources_runs_non_default_sqlite_with_path_injection_attack_ge
             module.borgmatic.actions.restore.Dump('sqlite_databases', 'database1'),
             module.borgmatic.actions.restore.Dump('sqlite_databases', 'database1'),
         ],
         ],
     ).once()
     ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/sqlite_databases',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
 
 
     assert (
     assert (
         module.dump_data_sources(
         module.dump_data_sources(
@@ -199,6 +227,13 @@ def test_dump_data_sources_with_non_existent_path_warns_and_dumps_database():
             module.borgmatic.actions.restore.Dump('sqlite_databases', 'database1'),
             module.borgmatic.actions.restore.Dump('sqlite_databases', 'database1'),
         ],
         ],
     ).once()
     ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/sqlite_databases',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
 
 
     assert (
     assert (
         module.dump_data_sources(
         module.dump_data_sources(
@@ -236,6 +271,13 @@ def test_dump_data_sources_with_name_all_warns_and_dumps_all_databases():
             module.borgmatic.actions.restore.Dump('sqlite_databases', 'all'),
             module.borgmatic.actions.restore.Dump('sqlite_databases', 'all'),
         ],
         ],
     ).once()
     ).once()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
+        object,
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/sqlite_databases',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
 
 
     assert (
     assert (
         module.dump_data_sources(
         module.dump_data_sources(
@@ -261,6 +303,7 @@ def test_dump_data_sources_does_not_dump_if_dry_run():
     flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
     flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
     flexmock(module).should_receive('execute_command').never()
     flexmock(module).should_receive('execute_command').never()
     flexmock(module.dump).should_receive('write_data_source_dumps_metadata').never()
     flexmock(module.dump).should_receive('write_data_source_dumps_metadata').never()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').never()
 
 
     assert (
     assert (
         module.dump_data_sources(
         module.dump_data_sources(

+ 27 - 11
tests/unit/hooks/data_source/test_zfs.py

@@ -1,5 +1,3 @@
-import os
-
 import pytest
 import pytest
 from flexmock import flexmock
 from flexmock import flexmock
 
 
@@ -302,7 +300,7 @@ def test_make_borg_snapshot_pattern_includes_slashdot_hack_and_stripped_pattern_
     )
     )
 
 
 
 
-def test_dump_data_sources_snapshots_and_mounts_and_updates_patterns():
+def test_dump_data_sources_snapshots_and_mounts_and_replaces_patterns():
     dataset = flexmock(
     dataset = flexmock(
         name='dataset',
         name='dataset',
         mount_point='/mnt/dataset',
         mount_point='/mnt/dataset',
@@ -330,6 +328,14 @@ def test_dump_data_sources_snapshots_and_mounts_and_updates_patterns():
         '/run/borgmatic',
         '/run/borgmatic',
     ).and_return(Pattern('/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir'))
     ).and_return(Pattern('/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir'))
     patterns = [Pattern('/mnt/dataset/subdir')]
     patterns = [Pattern('/mnt/dataset/subdir')]
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
+        object,
+        Pattern('/mnt/dataset/subdir'),
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
 
 
     assert (
     assert (
         module.dump_data_sources(
         module.dump_data_sources(
@@ -343,14 +349,13 @@ def test_dump_data_sources_snapshots_and_mounts_and_updates_patterns():
         == []
         == []
     )
     )
 
 
-    assert patterns == [Pattern(os.path.join(snapshot_mount_path, 'subdir'))]
-
 
 
 def test_dump_data_sources_with_no_datasets_skips_snapshots():
 def test_dump_data_sources_with_no_datasets_skips_snapshots():
     flexmock(module).should_receive('get_datasets_to_backup').and_return(())
     flexmock(module).should_receive('get_datasets_to_backup').and_return(())
     flexmock(module.os).should_receive('getpid').and_return(1234)
     flexmock(module.os).should_receive('getpid').and_return(1234)
     flexmock(module).should_receive('snapshot_dataset').never()
     flexmock(module).should_receive('snapshot_dataset').never()
     flexmock(module).should_receive('mount_snapshot').never()
     flexmock(module).should_receive('mount_snapshot').never()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').never()
     patterns = [Pattern('/mnt/dataset')]
     patterns = [Pattern('/mnt/dataset')]
 
 
     assert (
     assert (
@@ -395,6 +400,14 @@ def test_dump_data_sources_uses_custom_commands():
         dataset,
         dataset,
         '/run/borgmatic',
         '/run/borgmatic',
     ).and_return(Pattern('/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir'))
     ).and_return(Pattern('/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir'))
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
+        object,
+        Pattern('/mnt/dataset/subdir'),
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
     patterns = [Pattern('/mnt/dataset/subdir')]
     patterns = [Pattern('/mnt/dataset/subdir')]
     hook_config = {
     hook_config = {
         'zfs_command': '/usr/local/bin/zfs',
         'zfs_command': '/usr/local/bin/zfs',
@@ -416,8 +429,6 @@ def test_dump_data_sources_uses_custom_commands():
         == []
         == []
     )
     )
 
 
-    assert patterns == [Pattern(os.path.join(snapshot_mount_path, 'subdir'))]
-
 
 
 def test_dump_data_sources_with_dry_run_skips_commands_and_does_not_touch_patterns():
 def test_dump_data_sources_with_dry_run_skips_commands_and_does_not_touch_patterns():
     flexmock(module).should_receive('get_datasets_to_backup').and_return(
     flexmock(module).should_receive('get_datasets_to_backup').and_return(
@@ -426,6 +437,7 @@ def test_dump_data_sources_with_dry_run_skips_commands_and_does_not_touch_patter
     flexmock(module.os).should_receive('getpid').and_return(1234)
     flexmock(module.os).should_receive('getpid').and_return(1234)
     flexmock(module).should_receive('snapshot_dataset').never()
     flexmock(module).should_receive('snapshot_dataset').never()
     flexmock(module).should_receive('mount_snapshot').never()
     flexmock(module).should_receive('mount_snapshot').never()
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').never()
     patterns = [Pattern('/mnt/dataset')]
     patterns = [Pattern('/mnt/dataset')]
 
 
     assert (
     assert (
@@ -440,8 +452,6 @@ def test_dump_data_sources_with_dry_run_skips_commands_and_does_not_touch_patter
         == []
         == []
     )
     )
 
 
-    assert patterns == [Pattern('/mnt/dataset')]
-
 
 
 def test_dump_data_sources_ignores_mismatch_between_given_patterns_and_contained_patterns():
 def test_dump_data_sources_ignores_mismatch_between_given_patterns_and_contained_patterns():
     dataset = flexmock(
     dataset = flexmock(
@@ -470,6 +480,14 @@ def test_dump_data_sources_ignores_mismatch_between_given_patterns_and_contained
         dataset,
         dataset,
         '/run/borgmatic',
         '/run/borgmatic',
     ).and_return(Pattern('/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir'))
     ).and_return(Pattern('/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir'))
+    flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
+        object,
+        Pattern('/mnt/dataset/subdir'),
+        module.borgmatic.borg.pattern.Pattern(
+            '/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir',
+            source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
+        ),
+    ).once()
     patterns = [Pattern('/hmm')]
     patterns = [Pattern('/hmm')]
 
 
     assert (
     assert (
@@ -484,8 +502,6 @@ def test_dump_data_sources_ignores_mismatch_between_given_patterns_and_contained
         == []
         == []
     )
     )
 
 
-    assert patterns == [Pattern('/hmm'), Pattern(os.path.join(snapshot_mount_path, 'subdir'))]
-
 
 
 def test_get_all_snapshots_parses_list_output():
 def test_get_all_snapshots_parses_list_output():
     flexmock(module.borgmatic.execute).should_receive(
     flexmock(module.borgmatic.execute).should_receive(

+ 7 - 1
tox.ini

@@ -33,12 +33,18 @@ pass_env = COVERAGE_FILE
 commands =
 commands =
     pytest {posargs} --no-cov tests/end-to-end
     pytest {posargs} --no-cov tests/end-to-end
 
 
-[testenv:lint-fix]
+[testenv:lint]
 deps = []
 deps = []
 skip_install = true
 skip_install = true
 commands =
 commands =
     ruff check --diff {posargs}
     ruff check --diff {posargs}
 
 
+[testenv:lint-fix]
+deps = []
+skip_install = true
+commands =
+    ruff check --fix {posargs}
+
 [testenv:format]
 [testenv:format]
 deps = []
 deps = []
 skip_install = true
 skip_install = true