Browse Source

Update the logic that probes for the borgmatic streaming database dump, bootstrap metadata, and check state directories to support more platforms and use cases (#934).

Dan Helfman 6 months ago
parent
commit
295bfb0c57

+ 4 - 0
NEWS

@@ -1,5 +1,9 @@
 1.9.2.dev0
  * #932: Fix missing build backend setting in pyproject.toml to allow Fedora builds.
+ * #934: Update the logic that probes for the borgmatic streaming database dump, bootstrap
+   metadata, and check state directories to support more platforms and use cases.
+ * #934: Add the "RuntimeDirectory" and "StateDirectory" options to the sample systemd service
+   file to support the new runtime and state directory logic.
 
 1.9.1
  * #928: Fix the user runtime directory location on macOS (and possibly Cygwin).

+ 25 - 15
borgmatic/actions/check.py

@@ -403,16 +403,22 @@ BORG_DIRECTORY_FILE_TYPE = 'd'
 
 
 def collect_spot_check_archive_paths(
-    repository, archive, config, local_borg_version, global_arguments, local_path, remote_path
+    repository,
+    archive,
+    config,
+    local_borg_version,
+    global_arguments,
+    local_path,
+    remote_path,
+    borgmatic_runtime_directory,
 ):
     '''
     Given a repository configuration dict, the name of the latest archive, a configuration dict, the
-    local Borg version, global arguments as an argparse.Namespace instance, the local Borg path, and
-    the remote Borg path, collect the paths from the given archive (but only include files and
-    symlinks and exclude borgmatic runtime directories).
+    local Borg version, global arguments as an argparse.Namespace instance, the local Borg path, the
+    remote Borg path, and the borgmatic runtime directory, collect the paths from the given archive
+    (but only include files and symlinks and exclude borgmatic runtime directories).
     '''
     borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
-    borgmatic_runtime_directory = borgmatic.config.paths.get_borgmatic_runtime_directory(config)
 
     return tuple(
         path
@@ -546,11 +552,12 @@ def spot_check(
     global_arguments,
     local_path,
     remote_path,
+    borgmatic_runtime_directory,
 ):
     '''
     Given a repository dict, a loaded configuration dict, the local Borg version, global arguments
-    as an argparse.Namespace instance, the local Borg path, and the remote Borg path, perform a spot
-    check for the latest archive in the given repository.
+    as an argparse.Namespace instance, the local Borg path, the remote Borg path, and the borgmatic
+    runtime directory, perform a spot check for the latest archive in the given repository.
 
     A spot check compares file counts and also the hashes for a random sampling of source files on
     disk to those stored in the latest archive. If any differences are beyond configured tolerances,
@@ -600,6 +607,7 @@ def spot_check(
         global_arguments,
         local_path,
         remote_path,
+        borgmatic_runtime_directory,
     )
     logger.debug(f'{log_label}: {len(archive_paths)} total archive paths for spot check')
 
@@ -730,14 +738,16 @@ def run_check(
         write_check_time(make_check_time_path(config, repository_id, 'extract'))
 
     if 'spot' in checks:
-        spot_check(
-            repository,
-            config,
-            local_borg_version,
-            global_arguments,
-            local_path,
-            remote_path,
-        )
+        with borgmatic.config.paths.Runtime_directory(config) as borgmatic_runtime_directory:
+            spot_check(
+                repository,
+                config,
+                local_borg_version,
+                global_arguments,
+                local_path,
+                remote_path,
+                borgmatic_runtime_directory,
+            )
         write_check_time(make_check_time_path(config, repository_id, 'spot'))
 
     borgmatic.hooks.command.execute_hook(

+ 30 - 26
borgmatic/actions/config/bootstrap.py

@@ -38,37 +38,41 @@ def get_config_paths(archive_name, bootstrap_arguments, global_arguments, local_
     borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(
         {'borgmatic_source_directory': bootstrap_arguments.borgmatic_source_directory}
     )
-    borgmatic_runtime_directory = borgmatic.config.paths.get_borgmatic_runtime_directory(
-        {'user_runtime_directory': bootstrap_arguments.user_runtime_directory}
-    )
     config = make_bootstrap_config(bootstrap_arguments)
 
     # Probe for the manifest file in multiple locations, as the default location has moved to the
     # borgmatic runtime directory (which get stored as just "/borgmatic" with Borg 1.4+). But we
     # still want to support reading the manifest from previously created archives as well.
-    for base_directory in ('borgmatic', borgmatic_runtime_directory, borgmatic_source_directory):
-        borgmatic_manifest_path = os.path.join(base_directory, 'bootstrap', 'manifest.json')
-
-        extract_process = borgmatic.borg.extract.extract_archive(
-            global_arguments.dry_run,
-            bootstrap_arguments.repository,
-            archive_name,
-            [borgmatic_manifest_path],
-            config,
-            local_borg_version,
-            global_arguments,
-            local_path=bootstrap_arguments.local_path,
-            remote_path=bootstrap_arguments.remote_path,
-            extract_to_stdout=True,
-        )
-        manifest_json = extract_process.stdout.read()
-
-        if manifest_json:
-            break
-    else:
-        raise ValueError(
-            'Cannot read configuration paths from archive due to missing bootstrap manifest'
-        )
+    with borgmatic.config.paths.Runtime_directory(
+        {'user_runtime_directory': bootstrap_arguments.user_runtime_directory}
+    ) as borgmatic_runtime_directory:
+        for base_directory in (
+            'borgmatic',
+            borgmatic_runtime_directory,
+            borgmatic_source_directory,
+        ):
+            borgmatic_manifest_path = os.path.join(base_directory, 'bootstrap', 'manifest.json')
+
+            extract_process = borgmatic.borg.extract.extract_archive(
+                global_arguments.dry_run,
+                bootstrap_arguments.repository,
+                archive_name,
+                [borgmatic_manifest_path],
+                config,
+                local_borg_version,
+                global_arguments,
+                local_path=bootstrap_arguments.local_path,
+                remote_path=bootstrap_arguments.remote_path,
+                extract_to_stdout=True,
+            )
+            manifest_json = extract_process.stdout.read()
+
+            if manifest_json:
+                break
+        else:
+            raise ValueError(
+                'Cannot read configuration paths from archive due to missing bootstrap manifest'
+            )
 
     try:
         manifest_data = json.loads(manifest_json)

+ 56 - 46
borgmatic/actions/create.py

@@ -14,15 +14,15 @@ import borgmatic.hooks.dump
 logger = logging.getLogger(__name__)
 
 
-def create_borgmatic_manifest(config, config_paths, dry_run):
+def create_borgmatic_manifest(config, config_paths, borgmatic_runtime_directory, dry_run):
     '''
-    Create a borgmatic manifest file to store the paths to the configuration files used to create
-    the archive.
+    Given a configuration dict, a sequence of config file paths, the borgmatic runtime directory,
+    and whether this is a dry run, create a borgmatic manifest file to store the paths to the
+    configuration files used to create the archive.
     '''
     if dry_run:
         return
 
-    borgmatic_runtime_directory = borgmatic.config.paths.get_borgmatic_runtime_directory(config)
     borgmatic_manifest_path = os.path.join(
         borgmatic_runtime_directory, 'bootstrap', 'manifest.json'
     )
@@ -72,53 +72,63 @@ def run_create(
         **hook_context,
     )
     logger.info(f'{repository.get("label", repository["path"])}: Creating archive{dry_run_label}')
-    borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
-        'remove_data_source_dumps',
-        config,
-        repository['path'],
-        borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
-        global_arguments.dry_run,
-    )
-    active_dumps = borgmatic.hooks.dispatch.call_hooks(
-        'dump_data_sources',
-        config,
-        repository['path'],
-        borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
-        global_arguments.dry_run,
-    )
-    if config.get('store_config_files', True):
-        create_borgmatic_manifest(
+
+    with borgmatic.config.paths.Runtime_directory(config) as borgmatic_runtime_directory:
+        borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
+            'remove_data_source_dumps',
             config,
-            config_paths,
+            repository['path'],
+            borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
+            borgmatic_runtime_directory,
             global_arguments.dry_run,
         )
-    stream_processes = [process for processes in active_dumps.values() for process in processes]
+        active_dumps = borgmatic.hooks.dispatch.call_hooks(
+            'dump_data_sources',
+            config,
+            repository['path'],
+            borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
+            borgmatic_runtime_directory,
+            global_arguments.dry_run,
+        )
+        stream_processes = [process for processes in active_dumps.values() for process in processes]
 
-    json_output = borgmatic.borg.create.create_archive(
-        global_arguments.dry_run,
-        repository['path'],
-        config,
-        config_paths,
-        local_borg_version,
-        global_arguments,
-        local_path=local_path,
-        remote_path=remote_path,
-        progress=create_arguments.progress,
-        stats=create_arguments.stats,
-        json=create_arguments.json,
-        list_files=create_arguments.list_files,
-        stream_processes=stream_processes,
-    )
-    if json_output:
-        yield borgmatic.actions.json.parse_json(json_output, repository.get('label'))
+        if config.get('store_config_files', True):
+            create_borgmatic_manifest(
+                config,
+                config_paths,
+                borgmatic_runtime_directory,
+                global_arguments.dry_run,
+            )
+
+        json_output = borgmatic.borg.create.create_archive(
+            global_arguments.dry_run,
+            repository['path'],
+            config,
+            config_paths,
+            local_borg_version,
+            global_arguments,
+            borgmatic_runtime_directory,
+            local_path=local_path,
+            remote_path=remote_path,
+            progress=create_arguments.progress,
+            stats=create_arguments.stats,
+            json=create_arguments.json,
+            list_files=create_arguments.list_files,
+            stream_processes=stream_processes,
+        )
+
+        if json_output:
+            yield borgmatic.actions.json.parse_json(json_output, repository.get('label'))
+
+        borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
+            'remove_data_source_dumps',
+            config,
+            config_filename,
+            borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
+            borgmatic_runtime_directory,
+            global_arguments.dry_run,
+        )
 
-    borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
-        'remove_data_source_dumps',
-        config,
-        config_filename,
-        borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
-        global_arguments.dry_run,
-    )
     borgmatic.hooks.command.execute_hook(
         config.get('after_backup'),
         config.get('umask'),

+ 107 - 100
borgmatic/actions/restore.py

@@ -108,6 +108,7 @@ def restore_single_data_source(
     hook_name,
     data_source,
     connection_params,
+    borgmatic_runtime_directory,
 ):
     '''
     Given (among other things) an archive name, a data source hook name, the hostname, port,
@@ -123,9 +124,9 @@ def restore_single_data_source(
         config,
         repository['path'],
         borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
+        borgmatic_runtime_directory,
         data_source['name'],
     )[hook_name]
-    borgmatic_runtime_directory = borgmatic.config.paths.get_borgmatic_runtime_directory(config)
 
     destination_path = (
         tempfile.mkdtemp(dir=borgmatic_runtime_directory)
@@ -135,7 +136,7 @@ def restore_single_data_source(
 
     try:
         # Kick off a single data source extract. If using a directory format, extract to a temporary
-        # directory. Otheriwes extract the single dump file to stdout.
+        # directory. Otherwise extract the single dump file to stdout.
         extract_process = borgmatic.borg.extract.extract_archive(
             dry_run=global_arguments.dry_run,
             repository=repository['path'],
@@ -181,17 +182,17 @@ def collect_archive_data_source_names(
     global_arguments,
     local_path,
     remote_path,
+    borgmatic_runtime_directory,
 ):
     '''
     Given a local or remote repository path, a resolved archive name, a configuration dict, the
-    local Borg version, global_arguments an argparse.Namespace, and local and remote Borg paths,
-    query the archive for the names of data sources it contains as dumps and return them as a dict
-    from hook name to a sequence of data source names.
+    local Borg version, global_arguments an argparse.Namespace, local and remote Borg paths, and the
+    borgmatic runtime directory, query the archive for the names of data sources it contains as
+    dumps and return them as a dict from hook name to a sequence of data source names.
     '''
     borgmatic_source_directory = str(
         pathlib.Path(borgmatic.config.paths.get_borgmatic_source_directory(config))
     )
-    borgmatic_runtime_directory = borgmatic.config.paths.get_borgmatic_runtime_directory(config)
 
     # Probe for the data source dumps in multiple locations, as the default location has moved to
     # the borgmatic runtime directory (which get stored as just "/borgmatic" with Borg 1.4+). But we
@@ -330,6 +331,7 @@ def run_restore(
     global_arguments,
     local_path,
     remote_path,
+    borgmatic_runtime_directory,
 ):
     '''
     Run the "restore" action for the given repository, but only if the repository matches the
@@ -346,105 +348,110 @@ def run_restore(
         f'{repository.get("label", repository["path"])}: Restoring data sources from archive {restore_arguments.archive}'
     )
 
-    borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
-        'remove_data_source_dumps',
-        config,
-        repository['path'],
-        borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
-        global_arguments.dry_run,
-    )
+    with borgmatic.config.paths.Runtime_directory(config) as borgmatic_runtime_directory:
+        borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
+            'remove_data_source_dumps',
+            config,
+            repository['path'],
+            borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
+            borgmatic_runtime_directory,
+            global_arguments.dry_run,
+        )
 
-    archive_name = borgmatic.borg.repo_list.resolve_archive_name(
-        repository['path'],
-        restore_arguments.archive,
-        config,
-        local_borg_version,
-        global_arguments,
-        local_path,
-        remote_path,
-    )
-    archive_data_source_names = collect_archive_data_source_names(
-        repository['path'],
-        archive_name,
-        config,
-        local_borg_version,
-        global_arguments,
-        local_path,
-        remote_path,
-    )
-    restore_names = find_data_sources_to_restore(
-        restore_arguments.data_sources, archive_data_source_names
-    )
-    found_names = set()
-    remaining_restore_names = {}
-    connection_params = {
-        'hostname': restore_arguments.hostname,
-        'port': restore_arguments.port,
-        'username': restore_arguments.username,
-        'password': restore_arguments.password,
-        'restore_path': restore_arguments.restore_path,
-    }
-
-    for hook_name, data_source_names in restore_names.items():
-        for data_source_name in data_source_names:
-            found_hook_name, found_data_source = get_configured_data_source(
-                config, archive_data_source_names, hook_name, data_source_name
-            )
+        archive_name = borgmatic.borg.repo_list.resolve_archive_name(
+            repository['path'],
+            restore_arguments.archive,
+            config,
+            local_borg_version,
+            global_arguments,
+            local_path,
+            remote_path,
+        )
+        archive_data_source_names = collect_archive_data_source_names(
+            repository['path'],
+            archive_name,
+            config,
+            local_borg_version,
+            global_arguments,
+            local_path,
+            remote_path,
+        )
+        restore_names = find_data_sources_to_restore(
+            restore_arguments.data_sources, archive_data_source_names
+        )
+        found_names = set()
+        remaining_restore_names = {}
+        connection_params = {
+            'hostname': restore_arguments.hostname,
+            'port': restore_arguments.port,
+            'username': restore_arguments.username,
+            'password': restore_arguments.password,
+            'restore_path': restore_arguments.restore_path,
+        }
 
-            if not found_data_source:
-                remaining_restore_names.setdefault(found_hook_name or hook_name, []).append(
-                    data_source_name
+        for hook_name, data_source_names in restore_names.items():
+            for data_source_name in data_source_names:
+                found_hook_name, found_data_source = get_configured_data_source(
+                    config, archive_data_source_names, hook_name, data_source_name
                 )
-                continue
-
-            found_names.add(data_source_name)
-            restore_single_data_source(
-                repository,
-                config,
-                local_borg_version,
-                global_arguments,
-                local_path,
-                remote_path,
-                archive_name,
-                found_hook_name or hook_name,
-                dict(found_data_source, **{'schemas': restore_arguments.schemas}),
-                connection_params,
-            )
 
-    # For any data sources that weren't found via exact matches in the configuration, try to
-    # fallback to "all" entries.
-    for hook_name, data_source_names in remaining_restore_names.items():
-        for data_source_name in data_source_names:
-            found_hook_name, found_data_source = get_configured_data_source(
-                config, archive_data_source_names, hook_name, data_source_name, 'all'
-            )
+                if not found_data_source:
+                    remaining_restore_names.setdefault(found_hook_name or hook_name, []).append(
+                        data_source_name
+                    )
+                    continue
+
+                found_names.add(data_source_name)
+                restore_single_data_source(
+                    repository,
+                    config,
+                    local_borg_version,
+                    global_arguments,
+                    local_path,
+                    remote_path,
+                    archive_name,
+                    found_hook_name or hook_name,
+                    dict(found_data_source, **{'schemas': restore_arguments.schemas}),
+                    connection_params,
+                    borgmatic_runtime_directory,
+                )
 
-            if not found_data_source:
-                continue
-
-            found_names.add(data_source_name)
-            data_source = copy.copy(found_data_source)
-            data_source['name'] = data_source_name
-
-            restore_single_data_source(
-                repository,
-                config,
-                local_borg_version,
-                global_arguments,
-                local_path,
-                remote_path,
-                archive_name,
-                found_hook_name or hook_name,
-                dict(data_source, **{'schemas': restore_arguments.schemas}),
-                connection_params,
-            )
+        # For any data sources that weren't found via exact matches in the configuration, try to
+        # fallback to "all" entries.
+        for hook_name, data_source_names in remaining_restore_names.items():
+            for data_source_name in data_source_names:
+                found_hook_name, found_data_source = get_configured_data_source(
+                    config, archive_data_source_names, hook_name, data_source_name, 'all'
+                )
 
-    borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
-        'remove_data_source_dumps',
-        config,
-        repository['path'],
-        borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
-        global_arguments.dry_run,
-    )
+                if not found_data_source:
+                    continue
+
+                found_names.add(data_source_name)
+                data_source = copy.copy(found_data_source)
+                data_source['name'] = data_source_name
+
+                restore_single_data_source(
+                    repository,
+                    config,
+                    local_borg_version,
+                    global_arguments,
+                    local_path,
+                    remote_path,
+                    archive_name,
+                    found_hook_name or hook_name,
+                    dict(data_source, **{'schemas': restore_arguments.schemas}),
+                    connection_params,
+                    borgmatic_runtime_directory,
+                )
+
+        borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
+            'remove_data_source_dumps',
+            config,
+            repository['path'],
+            borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
+            borgmatic_runtime_directory,
+            global_arguments.dry_run,
+        )
 
     ensure_data_sources_found(restore_names, remaining_restore_names, found_names)

+ 2 - 3
borgmatic/borg/create.py

@@ -504,6 +504,7 @@ def create_archive(
     config_paths,
     local_borg_version,
     global_arguments,
+    borgmatic_runtime_directory,
     local_path='borg',
     remote_path=None,
     progress=False,
@@ -524,9 +525,7 @@ def create_archive(
 
     working_directory = borgmatic.config.paths.get_working_directory(config)
     borgmatic_runtime_directories = expand_directories(
-        collect_borgmatic_runtime_directories(
-            borgmatic.config.paths.get_borgmatic_runtime_directory(config)
-        ),
+        collect_borgmatic_runtime_directories(borgmatic_runtime_directory),
         working_directory=working_directory,
     )
 

+ 2 - 2
borgmatic/commands/borgmatic.py

@@ -275,8 +275,8 @@ def run_actions(
     '''
     Given parsed command-line arguments as an argparse.ArgumentParser instance, the configuration
     filename, a configuration dict, a sequence of loaded configuration paths, local and remote paths
-    to Borg, a local Borg version string, and a repository name, run all actions from the
-    command-line arguments on the given repository.
+    to Borg, a local Borg version string, a repository name, and the borgmatic runtime directory,
+    run all actions from the command-line arguments on the given repository.
 
     Yield JSON output strings from executing any actions that produce JSON.
 

+ 67 - 22
borgmatic/config/paths.py

@@ -1,4 +1,8 @@
+import logging
 import os
+import tempfile
+
+logger = logging.getLogger(__name__)
 
 
 def expand_user_in_path(path):
@@ -26,28 +30,70 @@ def get_borgmatic_source_directory(config):
     return expand_user_in_path(config.get('borgmatic_source_directory') or '~/.borgmatic')
 
 
-def get_borgmatic_runtime_directory(config):
+class Runtime_directory:
     '''
-    Given a configuration dict, get the borgmatic runtime directory used for storing temporary
-    runtime data like streaming database dumps and bootstrap metadata. Defaults to
-    $XDG_RUNTIME_DIR/./borgmatic or $TMPDIR/./borgmatic or $TEMP/./borgmatic or
-    /run/user/$UID/./borgmatic.
-
-    The "/./" is taking advantage of a Borg feature such that the part of the path before the "/./"
-    does not get stored in the file path within an archive. That way, the path of the runtime
-    directory can change without leaving database dumps within an archive inaccessible.
+    A Python context manager for creating and cleaning up the borgmatic runtime directory used for
+    storing temporary runtime data like streaming database dumps and bootstrap metadata.
+
+    Example use as a context manager:
+
+        with borgmatic.config.paths.Runtime_directory(config) as borgmatic_runtime_directory:
+            do_something_with(borgmatic_runtime_directory)
+
+    For the scope of that "with" statement, the runtime directory is available. Afterwards, it
+    automatically gets cleaned up as necessary.
     '''
-    return expand_user_in_path(
-        os.path.join(
+
+    def __init__(self, config):
+        '''
+        Given a configuration dict, determine the borgmatic runtime directory, creating a secure,
+        temporary directory within it if necessary. Defaults to $XDG_RUNTIME_DIR/./borgmatic or
+        $RUNTIME_DIRECTORY/./borgmatic or $TMPDIR/borgmatic-[random]/./borgmatic or
+        $TEMP/borgmatic-[random]/./borgmatic or /tmp/borgmatic-[random]/./borgmatic where "[random]"
+        is a randomly generated string intended to avoid path collisions.
+
+        If XDG_RUNTIME_DIR or RUNTIME_DIRECTORY is set and already ends in "/borgmatic", then don't
+        tack on a second "/borgmatic" path component.
+
+        The "/./" is taking advantage of a Borg feature such that the part of the path before the "/./"
+        does not get stored in the file path within an archive. That way, the path of the runtime
+        directory can change without leaving database dumps within an archive inaccessible.
+        '''
+
+        runtime_directory = (
             config.get('user_runtime_directory')
-            or os.environ.get('XDG_RUNTIME_DIR')
-            or os.environ.get('TMPDIR')
-            or os.environ.get('TEMP')
-            or f'/run/user/{os.getuid()}',
-            '.',
-            'borgmatic',
+            or os.environ.get('XDG_RUNTIME_DIR')  # Set by PAM on Linux.
+            or os.environ.get('RUNTIME_DIRECTORY')  # Set by systemd if configured.
         )
-    )
+
+        if runtime_directory:
+            self.temporary_directory = None
+        else:
+            self.temporary_directory = tempfile.TemporaryDirectory(
+                prefix='borgmatic', dir=os.environ.get('TMPDIR') or os.environ.get('TEMP') or '/tmp'
+            )
+            runtime_directory = self.temporary_directory.name
+
+        (base_path, final_directory) = os.path.split(runtime_directory.rstrip(os.path.sep))
+
+        self.runtime_path = expand_user_in_path(
+            os.path.join(
+                base_path if final_directory == 'borgmatic' else runtime_directory, '.', 'borgmatic'
+            )
+        )
+
+    def __enter__(self):
+        '''
+        Return the borgmatic runtime path as a string.
+        '''
+        return self.runtime_path
+
+    def __exit__(self, exception, value, traceback):
+        '''
+        Delete any temporary directory that was created as part of initialization.
+        '''
+        if self.temporary_directory:
+            self.temporary_directory.cleanup()
 
 
 def get_borgmatic_state_directory(config):
@@ -59,10 +105,9 @@ def get_borgmatic_state_directory(config):
     return expand_user_in_path(
         os.path.join(
             config.get('user_state_directory')
-            or os.environ.get(
-                'XDG_STATE_HOME',
-                '~/.local/state',
-            ),
+            or os.environ.get('XDG_STATE_HOME')
+            or os.environ.get('STATE_DIRECTORY')  # Set by systemd if configured.
+            or '~/.local/state',
             'borgmatic',
         )
     )

+ 25 - 23
borgmatic/hooks/mariadb.py

@@ -14,15 +14,11 @@ from borgmatic.hooks import dump
 logger = logging.getLogger(__name__)
 
 
-def make_dump_path(config, base_directory=None):  # pragma: no cover
+def make_dump_path(base_directory):  # pragma: no cover
     '''
-    Given a configuration dict and an optional base directory, make the corresponding dump path. If
-    a base directory isn't provided, use the borgmatic runtime directory.
+    Given a base directory, make the corresponding dump path.
     '''
-    return dump.make_data_source_dump_path(
-        base_directory or borgmatic.config.paths.get_borgmatic_runtime_directory(config),
-        'mariadb_databases',
-    )
+    return dump.make_data_source_dump_path(base_directory, 'mariadb_databases')
 
 
 SYSTEM_DATABASE_NAMES = ('information_schema', 'mysql', 'performance_schema', 'sys')
@@ -126,12 +122,12 @@ def use_streaming(databases, config, log_prefix):
     return any(databases)
 
 
-def dump_data_sources(databases, config, log_prefix, dry_run):
+def dump_data_sources(databases, config, log_prefix, borgmatic_runtime_directory, dry_run):
     '''
     Dump the given MariaDB databases to a named pipe. The databases are supplied as a sequence of
     dicts, one dict describing each database as per the configuration schema. Use the given
-    configuration dict to construct the destination path and the given log prefix in any log
-    entries.
+    borgmatic runtime directory to construct the destination path and the given log prefix in any
+    log entries.
 
     Return a sequence of subprocess.Popen instances for the dump processes ready to spew to a named
     pipe. But if this is a dry run, then don't actually dump anything and return an empty sequence.
@@ -142,7 +138,7 @@ def dump_data_sources(databases, config, log_prefix, dry_run):
     logger.info(f'{log_prefix}: Dumping MariaDB databases{dry_run_label}')
 
     for database in databases:
-        dump_path = make_dump_path(config)
+        dump_path = make_dump_path(borgmatic_runtime_directory)
         extra_environment = {'MYSQL_PWD': database['password']} if 'password' in database else None
         dump_database_names = database_names_to_dump(
             database, extra_environment, log_prefix, dry_run
@@ -185,30 +181,36 @@ def dump_data_sources(databases, config, log_prefix, dry_run):
     return [process for process in processes if process]
 
 
-def remove_data_source_dumps(databases, config, log_prefix, dry_run):  # pragma: no cover
+def remove_data_source_dumps(
+    databases, config, log_prefix, borgmatic_runtime_directory, dry_run
+):  # pragma: no cover
     '''
-    Remove all database dump files for this hook regardless of the given databases. Use the given
-    configuration dict to construct the destination path and the log prefix in any log entries. If
-    this is a dry run, then don't actually remove anything.
+    Remove all database dump files for this hook regardless of the given databases. Use the
+    borgmatic_runtime_directory to construct the destination path and the log prefix in any log
+    entries. If this is a dry run, then don't actually remove anything.
     '''
-    dump.remove_data_source_dumps(make_dump_path(config), 'MariaDB', log_prefix, dry_run)
+    dump.remove_data_source_dumps(
+        make_dump_path(borgmatic_runtime_directory), 'MariaDB', log_prefix, dry_run
+    )
 
 
-def make_data_source_dump_patterns(databases, config, log_prefix, name=None):  # pragma: no cover
+def make_data_source_dump_patterns(
+    databases, config, log_prefix, borgmatic_runtime_directory, name=None
+):  # pragma: no cover
     '''
-    Given a sequence of configurations dicts, a configuration dict, a prefix to log with, and a
-    database name to match, return the corresponding glob patterns to match the database dump in an
-    archive.
+    Given a sequence of configurations dicts, a configuration dict, a prefix to log with, the
+    borgmatic runtime directory, and a database name to match, return the corresponding glob
+    patterns to match the database dump in an archive.
     '''
     borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
 
     return (
+        dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, hostname='*'),
         dump.make_data_source_dump_filename(
-            make_dump_path(config, 'borgmatic'), name, hostname='*'
+            make_dump_path(borgmatic_runtime_directory), name, hostname='*'
         ),
-        dump.make_data_source_dump_filename(make_dump_path(config), name, hostname='*'),
         dump.make_data_source_dump_filename(
-            make_dump_path(config, borgmatic_source_directory), name, hostname='*'
+            make_dump_path(borgmatic_source_directory), name, hostname='*'
         ),
     )
 

+ 25 - 23
borgmatic/hooks/mongodb.py

@@ -8,15 +8,11 @@ from borgmatic.hooks import dump
 logger = logging.getLogger(__name__)
 
 
-def make_dump_path(config, base_directory=None):  # pragma: no cover
+def make_dump_path(base_directory):  # pragma: no cover
     '''
-    Given a configuration dict and an optional base directory, make the corresponding dump path. If
-    a base directory isn't provided, use the borgmatic runtime directory.
+    Given a base directory, make the corresponding dump path.
     '''
-    return dump.make_data_source_dump_path(
-        base_directory or borgmatic.config.paths.get_borgmatic_runtime_directory(config),
-        'mongodb_databases',
-    )
+    return dump.make_data_source_dump_path(base_directory, 'mongodb_databases')
 
 
 def use_streaming(databases, config, log_prefix):
@@ -27,11 +23,11 @@ def use_streaming(databases, config, log_prefix):
     return any(database.get('format') != 'directory' for database in databases)
 
 
-def dump_data_sources(databases, config, log_prefix, dry_run):
+def dump_data_sources(databases, config, log_prefix, borgmatic_runtime_directory, dry_run):
     '''
     Dump the given MongoDB databases to a named pipe. The databases are supplied as a sequence of
-    dicts, one dict describing each database as per the configuration schema. Use the configuration
-    dict to construct the destination path and the given log prefix in any log entries.
+    dicts, one dict describing each database as per the configuration schema. Use the borgmatic
+    runtime directory to construct the destination path and the given log prefix in any log entries.
 
     Return a sequence of subprocess.Popen instances for the dump processes ready to spew to a named
     pipe. But if this is a dry run, then don't actually dump anything and return an empty sequence.
@@ -44,7 +40,7 @@ def dump_data_sources(databases, config, log_prefix, dry_run):
     for database in databases:
         name = database['name']
         dump_filename = dump.make_data_source_dump_filename(
-            make_dump_path(config), name, database.get('hostname')
+            make_dump_path(borgmatic_runtime_directory), name, database.get('hostname')
         )
         dump_format = database.get('format', 'archive')
 
@@ -94,30 +90,36 @@ def build_dump_command(database, dump_filename, dump_format):
     )
 
 
-def remove_data_source_dumps(databases, config, log_prefix, dry_run):  # pragma: no cover
+def remove_data_source_dumps(
+    databases, config, log_prefix, borgmatic_runtime_directory, dry_run
+):  # pragma: no cover
     '''
-    Remove all database dump files for this hook regardless of the given databases. Use the log
-    prefix in any log entries. Use the given configuration dict to construct the destination path.
-    If this is a dry run, then don't actually remove anything.
+    Remove all database dump files for this hook regardless of the given databases. Use the
+    borgmatic_runtime_directory to construct the destination path and the log prefix in any log
+    entries. If this is a dry run, then don't actually remove anything.
     '''
-    dump.remove_data_source_dumps(make_dump_path(config), 'MongoDB', log_prefix, dry_run)
+    dump.remove_data_source_dumps(
+        make_dump_path(borgmatic_runtime_directory), 'MongoDB', log_prefix, dry_run
+    )
 
 
-def make_data_source_dump_patterns(databases, config, log_prefix, name=None):  # pragma: no cover
+def make_data_source_dump_patterns(
+    databases, config, log_prefix, borgmatic_runtime_directory, name=None
+):  # pragma: no cover
     '''
-    Given a sequence of database configurations dicts, a configuration dict, a prefix to log with,
-    and a database name to match, return the corresponding glob patterns to match the database dump
-    in an archive.
+    Given a sequence of configurations dicts, a configuration dict, a prefix to log with, the
+    borgmatic runtime directory, and a database name to match, return the corresponding glob
+    patterns to match the database dump in an archive.
     '''
     borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
 
     return (
+        dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, hostname='*'),
         dump.make_data_source_dump_filename(
-            make_dump_path(config, 'borgmatic'), name, hostname='*'
+            make_dump_path(borgmatic_runtime_directory), name, hostname='*'
         ),
-        dump.make_data_source_dump_filename(make_dump_path(config), name, hostname='*'),
         dump.make_data_source_dump_filename(
-            make_dump_path(config, borgmatic_source_directory), name, hostname='*'
+            make_dump_path(borgmatic_source_directory), name, hostname='*'
         ),
     )
 

+ 25 - 22
borgmatic/hooks/mysql.py

@@ -14,15 +14,11 @@ from borgmatic.hooks import dump
 logger = logging.getLogger(__name__)
 
 
-def make_dump_path(config, base_directory=None):  # pragma: no cover
+def make_dump_path(base_directory):  # pragma: no cover
     '''
-    Given a configuration dict and an optional base directory, make the corresponding dump path. If
-    a base directory isn't provided, use the borgmatic runtime directory.
+    Given a base directory, make the corresponding dump path.
     '''
-    return dump.make_data_source_dump_path(
-        base_directory or borgmatic.config.paths.get_borgmatic_runtime_directory(config),
-        'mysql_databases',
-    )
+    return dump.make_data_source_dump_path(base_directory, 'mysql_databases')
 
 
 SYSTEM_DATABASE_NAMES = ('information_schema', 'mysql', 'performance_schema', 'sys')
@@ -125,11 +121,12 @@ def use_streaming(databases, config, log_prefix):
     return any(databases)
 
 
-def dump_data_sources(databases, config, log_prefix, dry_run):
+def dump_data_sources(databases, config, log_prefix, borgmatic_runtime_directory, dry_run):
     '''
     Dump the given MySQL/MariaDB databases to a named pipe. The databases are supplied as a sequence
     of dicts, one dict describing each database as per the configuration schema. Use the given
-    configuration dict to construct the destination path and the given log prefix in any log entries.
+    borgmatic runtime directory to construct the destination path and the given log prefix in any
+    log entries.
 
     Return a sequence of subprocess.Popen instances for the dump processes ready to spew to a named
     pipe. But if this is a dry run, then don't actually dump anything and return an empty sequence.
@@ -140,7 +137,7 @@ def dump_data_sources(databases, config, log_prefix, dry_run):
     logger.info(f'{log_prefix}: Dumping MySQL databases{dry_run_label}')
 
     for database in databases:
-        dump_path = make_dump_path(config)
+        dump_path = make_dump_path(borgmatic_runtime_directory)
         extra_environment = {'MYSQL_PWD': database['password']} if 'password' in database else None
         dump_database_names = database_names_to_dump(
             database, extra_environment, log_prefix, dry_run
@@ -183,30 +180,36 @@ def dump_data_sources(databases, config, log_prefix, dry_run):
     return [process for process in processes if process]
 
 
-def remove_data_source_dumps(databases, config, log_prefix, dry_run):  # pragma: no cover
+def remove_data_source_dumps(
+    databases, config, log_prefix, borgmatic_runtime_directory, dry_run
+):  # pragma: no cover
     '''
-    Remove all database dump files for this hook regardless of the given databases. Use the given
-    configuration dict to construct the destination path and the log prefix in any log entries. If
-    this is a dry run, then don't actually remove anything.
+    Remove all database dump files for this hook regardless of the given databases. Use the
+    borgmatic runtime directory to construct the destination path and the log prefix in any log
+    entries. If this is a dry run, then don't actually remove anything.
     '''
-    dump.remove_data_source_dumps(make_dump_path(config), 'MySQL', log_prefix, dry_run)
+    dump.remove_data_source_dumps(
+        make_dump_path(borgmatic_runtime_directory), 'MySQL', log_prefix, dry_run
+    )
 
 
-def make_data_source_dump_patterns(databases, config, log_prefix, name=None):  # pragma: no cover
+def make_data_source_dump_patterns(
+    databases, config, log_prefix, borgmatic_runtime_directory, name=None
+):  # pragma: no cover
     '''
-    Given a sequence of configurations dicts, a configuration dict, a prefix to log with, and a
-    database name to match, return the corresponding glob patterns to match the database dump in an
-    archive.
+    Given a sequence of configurations dicts, a configuration dict, a prefix to log with, the
+    borgmatic runtime directory, and a database name to match, return the corresponding glob
+    patterns to match the database dump in an archive.
     '''
     borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
 
     return (
+        dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, hostname='*'),
         dump.make_data_source_dump_filename(
-            make_dump_path(config, 'borgmatic'), name, hostname='*'
+            make_dump_path(borgmatic_runtime_directory), name, hostname='*'
         ),
-        dump.make_data_source_dump_filename(make_dump_path(config), name, hostname='*'),
         dump.make_data_source_dump_filename(
-            make_dump_path(config, borgmatic_source_directory), name, hostname='*'
+            make_dump_path(borgmatic_source_directory), name, hostname='*'
         ),
     )
 

+ 25 - 23
borgmatic/hooks/postgresql.py

@@ -16,15 +16,11 @@ from borgmatic.hooks import dump
 logger = logging.getLogger(__name__)
 
 
-def make_dump_path(config, base_directory=None):  # pragma: no cover
+def make_dump_path(base_directory):  # pragma: no cover
     '''
-    Given a configuration dict and an optional base directory, make the corresponding dump path. If
-    a base directory isn't provided, use the borgmatic runtime directory.
+    Given a base directory, make the corresponding dump path.
     '''
-    return dump.make_data_source_dump_path(
-        base_directory or borgmatic.config.paths.get_borgmatic_runtime_directory(config),
-        'postgresql_databases',
-    )
+    return dump.make_data_source_dump_path(base_directory, 'postgresql_databases')
 
 
 def make_extra_environment(database, restore_connection_params=None):
@@ -108,12 +104,12 @@ def use_streaming(databases, config, log_prefix):
     return any(database.get('format') != 'directory' for database in databases)
 
 
-def dump_data_sources(databases, config, log_prefix, dry_run):
+def dump_data_sources(databases, config, log_prefix, borgmatic_runtime_directory, dry_run):
     '''
     Dump the given PostgreSQL databases to a named pipe. The databases are supplied as a sequence of
     dicts, one dict describing each database as per the configuration schema. Use the given
-    configuration dict to construct the destination path and the given log prefix in any log
-    entries.
+    borgmatic runtime directory to construct the destination path and the given log prefix in any
+    log entries.
 
     Return a sequence of subprocess.Popen instances for the dump processes ready to spew to a named
     pipe. But if this is a dry run, then don't actually dump anything and return an empty sequence.
@@ -127,7 +123,7 @@ def dump_data_sources(databases, config, log_prefix, dry_run):
 
     for database in databases:
         extra_environment = make_extra_environment(database)
-        dump_path = make_dump_path(config)
+        dump_path = make_dump_path(borgmatic_runtime_directory)
         dump_database_names = database_names_to_dump(
             database, extra_environment, log_prefix, dry_run
         )
@@ -210,30 +206,36 @@ def dump_data_sources(databases, config, log_prefix, dry_run):
     return processes
 
 
-def remove_data_source_dumps(databases, config, log_prefix, dry_run):  # pragma: no cover
+def remove_data_source_dumps(
+    databases, config, log_prefix, borgmatic_runtime_directory, dry_run
+):  # pragma: no cover
     '''
-    Remove all database dump files for this hook regardless of the given databases. Use the given
-    configuration dict to construct the destination path and the log prefix in any log entries. If
-    this is a dry run, then don't actually remove anything.
+    Remove all database dump files for this hook regardless of the given databases. Use the
+    borgmatic runtime directory to construct the destination path and the log prefix in any log
+    entries. If this is a dry run, then don't actually remove anything.
     '''
-    dump.remove_data_source_dumps(make_dump_path(config), 'PostgreSQL', log_prefix, dry_run)
+    dump.remove_data_source_dumps(
+        make_dump_path(borgmatic_runtime_directory), 'PostgreSQL', log_prefix, dry_run
+    )
 
 
-def make_data_source_dump_patterns(databases, config, log_prefix, name=None):  # pragma: no cover
+def make_data_source_dump_patterns(
+    databases, config, log_prefix, borgmatic_runtime_directory, name=None
+):  # pragma: no cover
     '''
-    Given a sequence of configurations dicts, a configuration dict, a prefix to log with, and a
-    database name to match, return the corresponding glob patterns to match the database dump in an
-    archive.
+    Given a sequence of configurations dicts, a configuration dict, a prefix to log with, the
+    borgmatic runtime directory, and a database name to match, return the corresponding glob
+    patterns to match the database dump in an archive.
     '''
     borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
 
     return (
+        dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, hostname='*'),
         dump.make_data_source_dump_filename(
-            make_dump_path(config, 'borgmatic'), name, hostname='*'
+            make_dump_path(borgmatic_runtime_directory), name, hostname='*'
         ),
-        dump.make_data_source_dump_filename(make_dump_path(config), name, hostname='*'),
         dump.make_data_source_dump_filename(
-            make_dump_path(config, borgmatic_source_directory), name, hostname='*'
+            make_dump_path(borgmatic_source_directory), name, hostname='*'
         ),
     )
 

+ 25 - 23
borgmatic/hooks/sqlite.py

@@ -9,15 +9,11 @@ from borgmatic.hooks import dump
 logger = logging.getLogger(__name__)
 
 
-def make_dump_path(config, base_directory=None):  # pragma: no cover
+def make_dump_path(base_directory):  # pragma: no cover
     '''
-    Given a configuration dict and an optional base directory, make the corresponding dump path. If
-    a base directory isn't provided, use the borgmatic runtime directory.
+    Given a base directory, make the corresponding dump path.
     '''
-    return dump.make_data_source_dump_path(
-        base_directory or borgmatic.config.paths.get_borgmatic_runtime_directory(config),
-        'sqlite_databases',
-    )
+    return dump.make_data_source_dump_path(base_directory, 'sqlite_databases')
 
 
 def use_streaming(databases, config, log_prefix):
@@ -28,11 +24,11 @@ def use_streaming(databases, config, log_prefix):
     return any(databases)
 
 
-def dump_data_sources(databases, config, log_prefix, dry_run):
+def dump_data_sources(databases, config, log_prefix, borgmatic_runtime_directory, dry_run):
     '''
     Dump the given SQLite databases to a named pipe. The databases are supplied as a sequence of
-    configuration dicts, as per the configuration schema. Use the given configuration dict to
-    construct the destination path and the given log prefix in any log entries.
+    configuration dicts, as per the configuration schema. Use the given borgmatic runtime directory
+    to construct the destination path and the given log prefix in any log entries.
 
     Return a sequence of subprocess.Popen instances for the dump processes ready to spew to a named
     pipe. But if this is a dry run, then don't actually dump anything and return an empty sequence.
@@ -52,7 +48,7 @@ def dump_data_sources(databases, config, log_prefix, dry_run):
                 f'{log_prefix}: No SQLite database at {database_path}; an empty database will be created and dumped'
             )
 
-        dump_path = make_dump_path(config)
+        dump_path = make_dump_path(borgmatic_runtime_directory)
         dump_filename = dump.make_data_source_dump_filename(dump_path, database['name'])
 
         if os.path.exists(dump_filename):
@@ -80,30 +76,36 @@ def dump_data_sources(databases, config, log_prefix, dry_run):
     return processes
 
 
-def remove_data_source_dumps(databases, config, log_prefix, dry_run):  # pragma: no cover
+def remove_data_source_dumps(
+    databases, config, log_prefix, borgmatic_runtime_directory, dry_run
+):  # pragma: no cover
     '''
-    Remove the given SQLite database dumps from the filesystem. The databases are supplied as a
-    sequence of configuration dicts, as per the configuration schema. Use the given configuration
-    dict to construct the destination path and the given log prefix in any log entries. If this is a
-    dry run, then don't actually remove anything.
+    Remove all database dump files for this hook regardless of the given databases. Use the
+    borgmatic runtime directory to construct the destination path and the log prefix in any log
+    entries. If this is a dry run, then don't actually remove anything.
     '''
-    dump.remove_data_source_dumps(make_dump_path(config), 'SQLite', log_prefix, dry_run)
+    dump.remove_data_source_dumps(
+        make_dump_path(borgmatic_runtime_directory), 'SQLite', log_prefix, dry_run
+    )
 
 
-def make_data_source_dump_patterns(databases, config, log_prefix, name=None):  # pragma: no cover
+def make_data_source_dump_patterns(
+    databases, config, log_prefix, borgmatic_runtime_directory, name=None
+):  # pragma: no cover
     '''
-    Make a pattern that matches the given SQLite databases. The databases are supplied as a sequence
-    of configuration dicts, as per the configuration schema.
+    Given a sequence of configurations dicts, a configuration dict, a prefix to log with, the
+    borgmatic runtime directory, and a database name to match, return the corresponding glob
+    patterns to match the database dump in an archive.
     '''
     borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
 
     return (
+        dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, hostname='*'),
         dump.make_data_source_dump_filename(
-            make_dump_path(config, 'borgmatic'), name, hostname='*'
+            make_dump_path(borgmatic_runtime_directory), name, hostname='*'
         ),
-        dump.make_data_source_dump_filename(make_dump_path(config), name, hostname='*'),
         dump.make_data_source_dump_filename(
-            make_dump_path(config, borgmatic_source_directory), name, hostname='*'
+            make_dump_path(borgmatic_source_directory), name, hostname='*'
         ),
     )
 

+ 2 - 0
sample/systemd/borgmatic.service

@@ -9,6 +9,8 @@ Documentation=https://torsion.org/borgmatic/
 
 [Service]
 Type=oneshot
+RuntimeDirectory=borgmatic
+StateDirectory=borgmatic
 
 # Load single encrypted credential.
 LoadCredentialEncrypted=borgmatic.pw

+ 70 - 15
tests/unit/actions/config/test_bootstrap.py

@@ -16,9 +16,6 @@ def test_get_config_paths_returns_list_of_config_paths():
     flexmock(module.borgmatic.config.paths).should_receive(
         'get_borgmatic_source_directory'
     ).and_return('/source')
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_runtime_directory'
-    ).and_return('/runtime')
     flexmock(module).should_receive('make_bootstrap_config').and_return({})
     bootstrap_arguments = flexmock(
         repository='repo',
@@ -33,6 +30,9 @@ def test_get_config_paths_returns_list_of_config_paths():
         dry_run=False,
     )
     local_borg_version = flexmock()
+    flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
+        flexmock()
+    )
     extract_process = flexmock(
         stdout=flexmock(
             read=lambda: '{"config_paths": ["/borgmatic/config.yaml"]}',
@@ -47,13 +47,58 @@ def test_get_config_paths_returns_list_of_config_paths():
     ) == ['/borgmatic/config.yaml']
 
 
-def test_get_config_paths_translates_ssh_command_argument_to_config():
+def test_get_config_paths_probes_for_manifest():
     flexmock(module.borgmatic.config.paths).should_receive(
         'get_borgmatic_source_directory'
     ).and_return('/source')
+    flexmock(module).should_receive('make_bootstrap_config').and_return({})
+    bootstrap_arguments = flexmock(
+        repository='repo',
+        archive='archive',
+        ssh_command=None,
+        local_path='borg7',
+        remote_path='borg8',
+        borgmatic_source_directory=None,
+        user_runtime_directory=None,
+    )
+    global_arguments = flexmock(
+        dry_run=False,
+    )
+    local_borg_version = flexmock()
+    borgmatic_runtime_directory = flexmock()
+    flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
+        borgmatic_runtime_directory,
+    )
+    flexmock(module.os.path).should_receive('join').with_args(
+        'borgmatic', 'bootstrap', 'manifest.json'
+    ).and_return(flexmock()).once()
+    flexmock(module.os.path).should_receive('join').with_args(
+        borgmatic_runtime_directory, 'bootstrap', 'manifest.json'
+    ).and_return(flexmock()).once()
+    flexmock(module.os.path).should_receive('join').with_args(
+        '/source', 'bootstrap', 'manifest.json'
+    ).and_return(flexmock()).once()
+    manifest_missing_extract_process = flexmock(
+        stdout=flexmock(read=lambda: None),
+    )
+    manifest_found_extract_process = flexmock(
+        stdout=flexmock(
+            read=lambda: '{"config_paths": ["/borgmatic/config.yaml"]}',
+        ),
+    )
+    flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').and_return(
+        manifest_missing_extract_process
+    ).and_return(manifest_missing_extract_process).and_return(manifest_found_extract_process)
+
+    assert module.get_config_paths(
+        'archive', bootstrap_arguments, global_arguments, local_borg_version
+    ) == ['/borgmatic/config.yaml']
+
+
+def test_get_config_paths_translates_ssh_command_argument_to_config():
     flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_runtime_directory'
-    ).and_return('/runtime')
+        'get_borgmatic_source_directory'
+    ).and_return('/source')
     config = flexmock()
     flexmock(module).should_receive('make_bootstrap_config').and_return(config)
     bootstrap_arguments = flexmock(
@@ -69,6 +114,9 @@ def test_get_config_paths_translates_ssh_command_argument_to_config():
         dry_run=False,
     )
     local_borg_version = flexmock()
+    flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
+        flexmock()
+    )
     extract_process = flexmock(
         stdout=flexmock(
             read=lambda: '{"config_paths": ["/borgmatic/config.yaml"]}',
@@ -96,9 +144,6 @@ def test_get_config_paths_with_missing_manifest_raises_value_error():
     flexmock(module.borgmatic.config.paths).should_receive(
         'get_borgmatic_source_directory'
     ).and_return('/source')
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_runtime_directory'
-    ).and_return('/runtime')
     flexmock(module).should_receive('make_bootstrap_config').and_return({})
     bootstrap_arguments = flexmock(
         repository='repo',
@@ -113,6 +158,10 @@ def test_get_config_paths_with_missing_manifest_raises_value_error():
         dry_run=False,
     )
     local_borg_version = flexmock()
+    flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
+        flexmock()
+    )
+    flexmock(module.os.path).should_receive('join').and_return(flexmock())
     extract_process = flexmock(stdout=flexmock(read=lambda: ''))
     flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').and_return(
         extract_process
@@ -128,9 +177,6 @@ def test_get_config_paths_with_broken_json_raises_value_error():
     flexmock(module.borgmatic.config.paths).should_receive(
         'get_borgmatic_source_directory'
     ).and_return('/source')
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_runtime_directory'
-    ).and_return('/runtime')
     flexmock(module).should_receive('make_bootstrap_config').and_return({})
     bootstrap_arguments = flexmock(
         repository='repo',
@@ -145,6 +191,9 @@ def test_get_config_paths_with_broken_json_raises_value_error():
         dry_run=False,
     )
     local_borg_version = flexmock()
+    flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
+        flexmock()
+    )
     extract_process = flexmock(
         stdout=flexmock(read=lambda: '{"config_paths": ["/oops'),
     )
@@ -162,9 +211,6 @@ def test_get_config_paths_with_json_missing_key_raises_value_error():
     flexmock(module.borgmatic.config.paths).should_receive(
         'get_borgmatic_source_directory'
     ).and_return('/source')
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_runtime_directory'
-    ).and_return('/runtime')
     flexmock(module).should_receive('make_bootstrap_config').and_return({})
     bootstrap_arguments = flexmock(
         repository='repo',
@@ -179,6 +225,9 @@ def test_get_config_paths_with_json_missing_key_raises_value_error():
         dry_run=False,
     )
     local_borg_version = flexmock()
+    flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
+        flexmock()
+    )
     extract_process = flexmock(
         stdout=flexmock(read=lambda: '{}'),
     )
@@ -210,6 +259,9 @@ def test_run_bootstrap_does_not_raise():
         dry_run=False,
     )
     local_borg_version = flexmock()
+    flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
+        flexmock()
+    )
     extract_process = flexmock(
         stdout=flexmock(
             read=lambda: '{"config_paths": ["borgmatic/config.yaml"]}',
@@ -244,6 +296,9 @@ def test_run_bootstrap_translates_ssh_command_argument_to_config():
         dry_run=False,
     )
     local_borg_version = flexmock()
+    flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
+        flexmock()
+    )
     extract_process = flexmock(
         stdout=flexmock(
             read=lambda: '{"config_paths": ["borgmatic/config.yaml"]}',

+ 13 - 12
tests/unit/actions/test_check.py

@@ -713,9 +713,6 @@ def test_collect_spot_check_archive_paths_excludes_directories():
     flexmock(module.borgmatic.config.paths).should_receive(
         'get_borgmatic_source_directory'
     ).and_return('/home/user/.borgmatic')
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_runtime_directory'
-    ).and_return('/run/user/1001/borgmatic')
     flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
         (
             'f /etc/path',
@@ -732,6 +729,7 @@ def test_collect_spot_check_archive_paths_excludes_directories():
         global_arguments=flexmock(),
         local_path=flexmock(),
         remote_path=flexmock(),
+        borgmatic_runtime_directory='/run/user/1001/borgmatic',
     ) == ('/etc/path', '/etc/other')
 
 
@@ -739,9 +737,6 @@ def test_collect_spot_check_archive_paths_excludes_file_in_borgmatic_runtime_dir
     flexmock(module.borgmatic.config.paths).should_receive(
         'get_borgmatic_source_directory'
     ).and_return('/root/.borgmatic')
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_runtime_directory'
-    ).and_return('/run/user/0/borgmatic')
     flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
         (
             'f /etc/path',
@@ -757,6 +752,7 @@ def test_collect_spot_check_archive_paths_excludes_file_in_borgmatic_runtime_dir
         global_arguments=flexmock(),
         local_path=flexmock(),
         remote_path=flexmock(),
+        borgmatic_runtime_directory='/run/user/0/borgmatic',
     ) == ('/etc/path',)
 
 
@@ -764,9 +760,6 @@ def test_collect_spot_check_archive_paths_excludes_file_in_borgmatic_source_dire
     flexmock(module.borgmatic.config.paths).should_receive(
         'get_borgmatic_source_directory'
     ).and_return('/root/.borgmatic')
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_runtime_directory'
-    ).and_return('/run/user/0/borgmatic')
     flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
         (
             'f /etc/path',
@@ -782,6 +775,7 @@ def test_collect_spot_check_archive_paths_excludes_file_in_borgmatic_source_dire
         global_arguments=flexmock(),
         local_path=flexmock(),
         remote_path=flexmock(),
+        borgmatic_runtime_directory='/run/user/0/borgmatic',
     ) == ('/etc/path',)
 
 
@@ -789,9 +783,6 @@ def test_collect_spot_check_archive_paths_excludes_file_in_borgmatic_runtime_dir
     flexmock(module.borgmatic.config.paths).should_receive(
         'get_borgmatic_source_directory'
     ).and_return('/root.borgmatic')
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_runtime_directory'
-    ).and_return('/run/user/0/borgmatic')
     flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
         (
             'f /etc/path',
@@ -807,6 +798,7 @@ def test_collect_spot_check_archive_paths_excludes_file_in_borgmatic_runtime_dir
         global_arguments=flexmock(),
         local_path=flexmock(),
         remote_path=flexmock(),
+        borgmatic_runtime_directory='/run/user/0/borgmatic',
     ) == ('/etc/path',)
 
 
@@ -1149,6 +1141,7 @@ def test_spot_check_without_spot_configuration_errors():
             global_arguments=flexmock(),
             local_path=flexmock(),
             remote_path=flexmock(),
+            borgmatic_runtime_directory='/run/borgmatic',
         )
 
 
@@ -1161,6 +1154,7 @@ def test_spot_check_without_any_configuration_errors():
             global_arguments=flexmock(),
             local_path=flexmock(),
             remote_path=flexmock(),
+            borgmatic_runtime_directory='/run/borgmatic',
         )
 
 
@@ -1181,6 +1175,7 @@ def test_spot_check_data_tolerance_percenatge_greater_than_data_sample_percentag
             global_arguments=flexmock(),
             local_path=flexmock(),
             remote_path=flexmock(),
+            borgmatic_runtime_directory='/run/borgmatic',
         )
 
 
@@ -1212,6 +1207,7 @@ def test_spot_check_with_count_delta_greater_than_count_tolerance_percentage_err
             global_arguments=flexmock(),
             local_path=flexmock(),
             remote_path=flexmock(),
+            borgmatic_runtime_directory='/run/borgmatic',
         )
 
 
@@ -1244,6 +1240,7 @@ def test_spot_check_with_failing_percentage_greater_than_data_tolerance_percenta
             global_arguments=flexmock(),
             local_path=flexmock(),
             remote_path=flexmock(),
+            borgmatic_runtime_directory='/run/borgmatic',
         )
 
 
@@ -1275,6 +1272,7 @@ def test_spot_check_with_high_enough_tolerances_does_not_raise():
         global_arguments=flexmock(),
         local_path=flexmock(),
         remote_path=flexmock(),
+        borgmatic_runtime_directory='/run/borgmatic',
     )
 
 
@@ -1362,6 +1360,9 @@ def test_run_check_runs_configured_spot_check():
     flexmock(module).should_receive('make_archives_check_id').and_return(None)
     flexmock(module).should_receive('filter_checks_on_frequency').and_return({'spot'})
     flexmock(module.borgmatic.borg.check).should_receive('check_archives').never()
+    flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
+        flexmock()
+    )
     flexmock(module.borgmatic.actions.check).should_receive('spot_check').once()
     flexmock(module).should_receive('make_check_time_path')
     flexmock(module).should_receive('write_check_time')

+ 24 - 15
tests/unit/actions/test_create.py

@@ -8,6 +8,9 @@ from borgmatic.actions import create as module
 def test_run_create_executes_and_calls_hooks_for_configured_repository():
     flexmock(module.logger).answer = lambda message: None
     flexmock(module.borgmatic.config.validate).should_receive('repositories_match').never()
+    flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
+        flexmock()
+    )
     flexmock(module.borgmatic.borg.create).should_receive('create_archive').once()
     flexmock(module).should_receive('create_borgmatic_manifest').once()
     flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
@@ -44,6 +47,9 @@ def test_run_create_executes_and_calls_hooks_for_configured_repository():
 def test_run_create_with_store_config_files_false_does_not_create_borgmatic_manifest():
     flexmock(module.logger).answer = lambda message: None
     flexmock(module.borgmatic.config.validate).should_receive('repositories_match').never()
+    flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
+        flexmock()
+    )
     flexmock(module.borgmatic.borg.create).should_receive('create_archive').once()
     flexmock(module).should_receive('create_borgmatic_manifest').never()
     flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
@@ -82,6 +88,9 @@ def test_run_create_runs_with_selected_repository():
     flexmock(module.borgmatic.config.validate).should_receive(
         'repositories_match'
     ).once().and_return(True)
+    flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
+        flexmock()
+    )
     flexmock(module.borgmatic.borg.create).should_receive('create_archive').once()
     flexmock(module).should_receive('create_borgmatic_manifest').once()
     flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
@@ -120,6 +129,7 @@ def test_run_create_bails_if_repository_does_not_match():
     flexmock(module.borgmatic.config.validate).should_receive(
         'repositories_match'
     ).once().and_return(False)
+    flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').never()
     flexmock(module.borgmatic.borg.create).should_receive('create_archive').never()
     flexmock(module).should_receive('create_borgmatic_manifest').never()
     create_arguments = flexmock(
@@ -153,6 +163,9 @@ def test_run_create_produces_json():
     flexmock(module.borgmatic.config.validate).should_receive(
         'repositories_match'
     ).once().and_return(True)
+    flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
+        flexmock()
+    )
     flexmock(module.borgmatic.borg.create).should_receive('create_archive').once().and_return(
         flexmock()
     )
@@ -191,18 +204,15 @@ def test_run_create_produces_json():
 
 
 def test_create_borgmatic_manifest_creates_manifest_file():
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_runtime_directory'
-    ).and_return('/run/user/0/borgmatic')
     flexmock(module.os.path).should_receive('join').with_args(
-        '/run/user/0/borgmatic', 'bootstrap', 'manifest.json'
-    ).and_return('/run/user/0/borgmatic/bootstrap/manifest.json')
+        '/run/borgmatic', 'bootstrap', 'manifest.json'
+    ).and_return('/run/borgmatic/bootstrap/manifest.json')
     flexmock(module.os.path).should_receive('exists').and_return(False)
     flexmock(module.os).should_receive('makedirs').and_return(True)
 
     flexmock(module.importlib.metadata).should_receive('version').and_return('1.0.0')
     flexmock(sys.modules['builtins']).should_receive('open').with_args(
-        '/run/user/0/borgmatic/bootstrap/manifest.json', 'w'
+        '/run/borgmatic/bootstrap/manifest.json', 'w'
     ).and_return(
         flexmock(
             __enter__=lambda *args: flexmock(write=lambda *args: None, close=lambda *args: None),
@@ -211,22 +221,19 @@ def test_create_borgmatic_manifest_creates_manifest_file():
     )
     flexmock(module.json).should_receive('dump').and_return(True).once()
 
-    module.create_borgmatic_manifest({}, 'test.yaml', False)
+    module.create_borgmatic_manifest({}, 'test.yaml', '/run/borgmatic', False)
 
 
 def test_create_borgmatic_manifest_creates_manifest_file_with_custom_borgmatic_runtime_directory():
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_runtime_directory'
-    ).and_return('/borgmatic')
     flexmock(module.os.path).should_receive('join').with_args(
-        '/borgmatic', 'bootstrap', 'manifest.json'
-    ).and_return('/borgmatic/bootstrap/manifest.json')
+        '/run/borgmatic', 'bootstrap', 'manifest.json'
+    ).and_return('/run/borgmatic/bootstrap/manifest.json')
     flexmock(module.os.path).should_receive('exists').and_return(False)
     flexmock(module.os).should_receive('makedirs').and_return(True)
 
     flexmock(module.importlib.metadata).should_receive('version').and_return('1.0.0')
     flexmock(sys.modules['builtins']).should_receive('open').with_args(
-        '/borgmatic/bootstrap/manifest.json', 'w'
+        '/run/borgmatic/bootstrap/manifest.json', 'w'
     ).and_return(
         flexmock(
             __enter__=lambda *args: flexmock(write=lambda *args: None, close=lambda *args: None),
@@ -236,9 +243,11 @@ def test_create_borgmatic_manifest_creates_manifest_file_with_custom_borgmatic_r
     flexmock(module.json).should_receive('dump').and_return(True).once()
 
     module.create_borgmatic_manifest(
-        {'borgmatic_runtime_directory': '/borgmatic'}, 'test.yaml', False
+        {'borgmatic_runtime_directory': '/borgmatic'}, 'test.yaml', '/run/borgmatic', False
     )
 
 
 def test_create_borgmatic_manifest_does_not_create_manifest_file_on_dry_run():
-    module.create_borgmatic_manifest({}, 'test.yaml', True)
+    flexmock(module.json).should_receive('dump').never()
+
+    module.create_borgmatic_manifest({}, 'test.yaml', '/run/borgmatic', True)

+ 44 - 28
tests/unit/actions/test_restore.py

@@ -87,11 +87,8 @@ def test_strip_path_prefix_from_extracted_dump_destination_renames_first_matchin
 
 def test_restore_single_data_source_extracts_and_restores_single_file_dump():
     flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').with_args(
-        'make_data_source_dump_patterns', object, object, object, object
+        'make_data_source_dump_patterns', object, object, object, object, object
     ).and_return({'postgresql': flexmock()})
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_runtime_directory'
-    ).and_return('/run/user/0/borgmatic')
     flexmock(module.tempfile).should_receive('mkdtemp').never()
     flexmock(module.borgmatic.hooks.dump).should_receive(
         'convert_glob_patterns_to_borg_pattern'
@@ -123,16 +120,14 @@ def test_restore_single_data_source_extracts_and_restores_single_file_dump():
         hook_name='postgresql',
         data_source={'name': 'test', 'format': 'plain'},
         connection_params=flexmock(),
+        borgmatic_runtime_directory='/run/borgmatic',
     )
 
 
 def test_restore_single_data_source_extracts_and_restores_directory_dump():
     flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').with_args(
-        'make_data_source_dump_patterns', object, object, object, object
+        'make_data_source_dump_patterns', object, object, object, object, object
     ).and_return({'postgresql': flexmock()})
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_runtime_directory'
-    ).and_return('/run/user/0/borgmatic')
     flexmock(module.tempfile).should_receive('mkdtemp').once().and_return(
         '/run/user/0/borgmatic/tmp1234'
     )
@@ -166,16 +161,14 @@ def test_restore_single_data_source_extracts_and_restores_directory_dump():
         hook_name='postgresql',
         data_source={'name': 'test', 'format': 'directory'},
         connection_params=flexmock(),
+        borgmatic_runtime_directory='/run/borgmatic',
     )
 
 
 def test_restore_single_data_source_with_directory_dump_error_cleans_up_temporary_directory():
     flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').with_args(
-        'make_data_source_dump_patterns', object, object, object, object
+        'make_data_source_dump_patterns', object, object, object, object, object
     ).and_return({'postgresql': flexmock()})
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_runtime_directory'
-    ).and_return('/run/user/0/borgmatic')
     flexmock(module.tempfile).should_receive('mkdtemp').once().and_return(
         '/run/user/0/borgmatic/tmp1234'
     )
@@ -210,16 +203,14 @@ def test_restore_single_data_source_with_directory_dump_error_cleans_up_temporar
             hook_name='postgresql',
             data_source={'name': 'test', 'format': 'directory'},
             connection_params=flexmock(),
+            borgmatic_runtime_directory='/run/borgmatic',
         )
 
 
 def test_restore_single_data_source_with_directory_dump_and_dry_run_skips_directory_move_and_cleanup():
     flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').with_args(
-        'make_data_source_dump_patterns', object, object, object, object
+        'make_data_source_dump_patterns', object, object, object, object, object
     ).and_return({'postgresql': flexmock()})
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_runtime_directory'
-    ).and_return('/run/user/0/borgmatic')
     flexmock(module.tempfile).should_receive('mkdtemp').once().and_return(
         '/run/user/0/borgmatic/tmp1234'
     )
@@ -253,6 +244,7 @@ def test_restore_single_data_source_with_directory_dump_and_dry_run_skips_direct
         hook_name='postgresql',
         data_source={'name': 'test', 'format': 'directory'},
         connection_params=flexmock(),
+        borgmatic_runtime_directory='/run/borgmatic',
     )
 
 
@@ -260,9 +252,6 @@ def test_collect_archive_data_source_names_parses_archive_paths():
     flexmock(module.borgmatic.config.paths).should_receive(
         'get_borgmatic_source_directory'
     ).and_return('/root/.borgmatic')
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_runtime_directory'
-    ).and_return('/run/user/0/borgmatic')
     flexmock(module.borgmatic.hooks.dump).should_receive('make_data_source_dump_path').and_return(
         ''
     )
@@ -282,6 +271,7 @@ def test_collect_archive_data_source_names_parses_archive_paths():
         global_arguments=flexmock(log_json=False),
         local_path=flexmock(),
         remote_path=flexmock(),
+        borgmatic_runtime_directory='/run/borgmatic',
     )
 
     assert archive_data_source_names == {
@@ -294,9 +284,6 @@ def test_collect_archive_data_source_names_parses_archive_paths_with_different_b
     flexmock(module.borgmatic.config.paths).should_receive(
         'get_borgmatic_source_directory'
     ).and_return('/root/.borgmatic')
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_runtime_directory'
-    ).and_return('/run/user/0/borgmatic')
     flexmock(module.borgmatic.hooks.dump).should_receive('make_data_source_dump_path').and_return(
         ''
     )
@@ -317,6 +304,7 @@ def test_collect_archive_data_source_names_parses_archive_paths_with_different_b
         global_arguments=flexmock(log_json=False),
         local_path=flexmock(),
         remote_path=flexmock(),
+        borgmatic_runtime_directory='/run/borgmatic',
     )
 
     assert archive_data_source_names == {
@@ -329,9 +317,6 @@ def test_collect_archive_data_source_names_parses_directory_format_archive_paths
     flexmock(module.borgmatic.config.paths).should_receive(
         'get_borgmatic_source_directory'
     ).and_return('/root/.borgmatic')
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_runtime_directory'
-    ).and_return('/run/user/0/borgmatic')
     flexmock(module.borgmatic.hooks.dump).should_receive('make_data_source_dump_path').and_return(
         ''
     )
@@ -350,6 +335,7 @@ def test_collect_archive_data_source_names_parses_directory_format_archive_paths
         global_arguments=flexmock(log_json=False),
         local_path=flexmock(),
         remote_path=flexmock(),
+        borgmatic_runtime_directory='/run/borgmatic',
     )
 
     assert archive_data_source_names == {
@@ -361,9 +347,6 @@ def test_collect_archive_data_source_names_skips_bad_archive_paths():
     flexmock(module.borgmatic.config.paths).should_receive(
         'get_borgmatic_source_directory'
     ).and_return('/root/.borgmatic')
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_runtime_directory'
-    ).and_return('/run/user/0/borgmatic')
     flexmock(module.borgmatic.hooks.dump).should_receive('make_data_source_dump_path').and_return(
         ''
     )
@@ -384,6 +367,7 @@ def test_collect_archive_data_source_names_skips_bad_archive_paths():
         global_arguments=flexmock(log_json=False),
         local_path=flexmock(),
         remote_path=flexmock(),
+        borgmatic_runtime_directory='/run/borgmatic',
     )
 
     assert archive_data_source_names == {
@@ -481,6 +465,10 @@ def test_run_restore_restores_each_data_source():
     }
 
     flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
+    borgmatic_runtime_directory = flexmock()
+    flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
+        borgmatic_runtime_directory
+    )
     flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks_even_if_unconfigured')
     flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name').and_return(
         flexmock()
@@ -501,6 +489,7 @@ def test_run_restore_restores_each_data_source():
         hook_name='postgresql_databases',
         data_source={'name': 'foo', 'schemas': None},
         connection_params=object,
+        borgmatic_runtime_directory=borgmatic_runtime_directory,
     ).once()
     flexmock(module).should_receive('restore_single_data_source').with_args(
         repository=object,
@@ -513,6 +502,7 @@ def test_run_restore_restores_each_data_source():
         hook_name='postgresql_databases',
         data_source={'name': 'bar', 'schemas': None},
         connection_params=object,
+        borgmatic_runtime_directory=borgmatic_runtime_directory,
     ).once()
     flexmock(module).should_receive('ensure_data_sources_found')
 
@@ -534,6 +524,7 @@ def test_run_restore_restores_each_data_source():
         global_arguments=flexmock(dry_run=False),
         local_path=flexmock(),
         remote_path=flexmock(),
+        borgmatic_runtime_directory='/run/borgmatic',
     )
 
 
@@ -541,6 +532,9 @@ def test_run_restore_bails_for_non_matching_repository():
     flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(
         False
     )
+    flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
+        flexmock()
+    )
     flexmock(module.borgmatic.hooks.dispatch).should_receive(
         'call_hooks_even_if_unconfigured'
     ).never()
@@ -554,6 +548,7 @@ def test_run_restore_bails_for_non_matching_repository():
         global_arguments=flexmock(dry_run=False),
         local_path=flexmock(),
         remote_path=flexmock(),
+        borgmatic_runtime_directory='/run/borgmatic',
     )
 
 
@@ -563,6 +558,10 @@ def test_run_restore_restores_data_source_configured_with_all_name():
     }
 
     flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
+    borgmatic_runtime_directory = flexmock()
+    flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
+        borgmatic_runtime_directory
+    )
     flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks_even_if_unconfigured')
     flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name').and_return(
         flexmock()
@@ -599,6 +598,7 @@ def test_run_restore_restores_data_source_configured_with_all_name():
         hook_name='postgresql_databases',
         data_source={'name': 'foo', 'schemas': None},
         connection_params=object,
+        borgmatic_runtime_directory=borgmatic_runtime_directory,
     ).once()
     flexmock(module).should_receive('restore_single_data_source').with_args(
         repository=object,
@@ -611,6 +611,7 @@ def test_run_restore_restores_data_source_configured_with_all_name():
         hook_name='postgresql_databases',
         data_source={'name': 'bar', 'schemas': None},
         connection_params=object,
+        borgmatic_runtime_directory=borgmatic_runtime_directory,
     ).once()
     flexmock(module).should_receive('ensure_data_sources_found')
 
@@ -632,6 +633,7 @@ def test_run_restore_restores_data_source_configured_with_all_name():
         global_arguments=flexmock(dry_run=False),
         local_path=flexmock(),
         remote_path=flexmock(),
+        borgmatic_runtime_directory='/run/borgmatic',
     )
 
 
@@ -641,6 +643,10 @@ def test_run_restore_skips_missing_data_source():
     }
 
     flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
+    borgmatic_runtime_directory = flexmock()
+    flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
+        borgmatic_runtime_directory
+    )
     flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks_even_if_unconfigured')
     flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name').and_return(
         flexmock()
@@ -677,6 +683,7 @@ def test_run_restore_skips_missing_data_source():
         hook_name='postgresql_databases',
         data_source={'name': 'foo', 'schemas': None},
         connection_params=object,
+        borgmatic_runtime_directory=borgmatic_runtime_directory,
     ).once()
     flexmock(module).should_receive('restore_single_data_source').with_args(
         repository=object,
@@ -689,6 +696,7 @@ def test_run_restore_skips_missing_data_source():
         hook_name='postgresql_databases',
         data_source={'name': 'bar', 'schemas': None},
         connection_params=object,
+        borgmatic_runtime_directory=borgmatic_runtime_directory,
     ).never()
     flexmock(module).should_receive('ensure_data_sources_found')
 
@@ -710,6 +718,7 @@ def test_run_restore_skips_missing_data_source():
         global_arguments=flexmock(dry_run=False),
         local_path=flexmock(),
         remote_path=flexmock(),
+        borgmatic_runtime_directory='/run/borgmatic',
     )
 
 
@@ -720,6 +729,10 @@ def test_run_restore_restores_data_sources_from_different_hooks():
     }
 
     flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
+    borgmatic_runtime_directory = flexmock()
+    flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
+        borgmatic_runtime_directory
+    )
     flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks_even_if_unconfigured')
     flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name').and_return(
         flexmock()
@@ -749,6 +762,7 @@ def test_run_restore_restores_data_sources_from_different_hooks():
         hook_name='postgresql_databases',
         data_source={'name': 'foo', 'schemas': None},
         connection_params=object,
+        borgmatic_runtime_directory=borgmatic_runtime_directory,
     ).once()
     flexmock(module).should_receive('restore_single_data_source').with_args(
         repository=object,
@@ -761,6 +775,7 @@ def test_run_restore_restores_data_sources_from_different_hooks():
         hook_name='mysql_databases',
         data_source={'name': 'bar', 'schemas': None},
         connection_params=object,
+        borgmatic_runtime_directory=borgmatic_runtime_directory,
     ).once()
     flexmock(module).should_receive('ensure_data_sources_found')
 
@@ -782,4 +797,5 @@ def test_run_restore_restores_data_sources_from_different_hooks():
         global_arguments=flexmock(dry_run=False),
         local_path=flexmock(),
         remote_path=flexmock(),
+        borgmatic_runtime_directory='/run/borgmatic',
     )

+ 17 - 51
tests/unit/borg/test_create.py

@@ -1453,9 +1453,6 @@ def test_create_archive_calls_borg_with_parameters():
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('expand_directories').and_return(())
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_runtime_directory'
-    ).and_return('/var/run/0/borgmatic')
     flexmock(module).should_receive('collect_borgmatic_runtime_directories').and_return([])
     flexmock(module).should_receive('make_base_create_command').and_return(
         (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
@@ -1483,6 +1480,7 @@ def test_create_archive_calls_borg_with_parameters():
         config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
         global_arguments=flexmock(log_json=False),
+        borgmatic_runtime_directory='/borgmatic/run',
     )
 
 
@@ -1490,9 +1488,6 @@ def test_create_archive_calls_borg_with_environment():
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('expand_directories').and_return(())
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_runtime_directory'
-    ).and_return('/var/run/0/borgmatic')
     flexmock(module).should_receive('collect_borgmatic_runtime_directories').and_return([])
     flexmock(module).should_receive('make_base_create_command').and_return(
         (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
@@ -1521,6 +1516,7 @@ def test_create_archive_calls_borg_with_environment():
         config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
         global_arguments=flexmock(log_json=False),
+        borgmatic_runtime_directory='/borgmatic/run',
     )
 
 
@@ -1528,9 +1524,6 @@ def test_create_archive_with_log_info_calls_borg_with_info_parameter():
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('expand_directories').and_return(())
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_runtime_directory'
-    ).and_return('/var/run/0/borgmatic')
     flexmock(module).should_receive('collect_borgmatic_runtime_directories').and_return([])
     flexmock(module).should_receive('make_base_create_command').and_return(
         (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
@@ -1559,6 +1552,7 @@ def test_create_archive_with_log_info_calls_borg_with_info_parameter():
         config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
         global_arguments=flexmock(log_json=False),
+        borgmatic_runtime_directory='/borgmatic/run',
     )
 
 
@@ -1566,9 +1560,6 @@ def test_create_archive_with_log_info_and_json_suppresses_most_borg_output():
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('expand_directories').and_return(())
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_runtime_directory'
-    ).and_return('/var/run/0/borgmatic')
     flexmock(module).should_receive('collect_borgmatic_runtime_directories').and_return([])
     flexmock(module).should_receive('make_base_create_command').and_return(
         (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
@@ -1595,6 +1586,7 @@ def test_create_archive_with_log_info_and_json_suppresses_most_borg_output():
         config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
         global_arguments=flexmock(log_json=False),
+        borgmatic_runtime_directory='/borgmatic/run',
         json=True,
     )
 
@@ -1603,9 +1595,6 @@ def test_create_archive_with_log_debug_calls_borg_with_debug_parameter():
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('expand_directories').and_return(())
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_runtime_directory'
-    ).and_return('/var/run/0/borgmatic')
     flexmock(module).should_receive('collect_borgmatic_runtime_directories').and_return([])
     flexmock(module).should_receive('make_base_create_command').and_return(
         (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
@@ -1634,6 +1623,7 @@ def test_create_archive_with_log_debug_calls_borg_with_debug_parameter():
         config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
         global_arguments=flexmock(log_json=False),
+        borgmatic_runtime_directory='/borgmatic/run',
     )
 
 
@@ -1641,9 +1631,6 @@ def test_create_archive_with_log_debug_and_json_suppresses_most_borg_output():
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('expand_directories').and_return(())
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_runtime_directory'
-    ).and_return('/var/run/0/borgmatic')
     flexmock(module).should_receive('collect_borgmatic_runtime_directories').and_return([])
     flexmock(module).should_receive('make_base_create_command').and_return(
         (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
@@ -1670,6 +1657,7 @@ def test_create_archive_with_log_debug_and_json_suppresses_most_borg_output():
         config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
         global_arguments=flexmock(log_json=False),
+        borgmatic_runtime_directory='/borgmatic/run',
         json=True,
     )
 
@@ -1680,9 +1668,6 @@ def test_create_archive_with_stats_and_dry_run_calls_borg_without_stats():
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('expand_directories').and_return(())
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_runtime_directory'
-    ).and_return('/var/run/0/borgmatic')
     flexmock(module).should_receive('collect_borgmatic_runtime_directories').and_return([])
     flexmock(module).should_receive('make_base_create_command').and_return(
         (('borg', 'create', '--dry-run'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
@@ -1711,6 +1696,7 @@ def test_create_archive_with_stats_and_dry_run_calls_borg_without_stats():
         config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
         global_arguments=flexmock(log_json=False),
+        borgmatic_runtime_directory='/borgmatic/run',
         stats=True,
     )
 
@@ -1719,9 +1705,6 @@ def test_create_archive_with_working_directory_calls_borg_with_working_directory
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('expand_directories').and_return(())
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_runtime_directory'
-    ).and_return('/var/run/0/borgmatic')
     flexmock(module).should_receive('collect_borgmatic_runtime_directories').and_return([])
     flexmock(module).should_receive('make_base_create_command').and_return(
         (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
@@ -1752,6 +1735,7 @@ def test_create_archive_with_working_directory_calls_borg_with_working_directory
         config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
         global_arguments=flexmock(log_json=False),
+        borgmatic_runtime_directory='/borgmatic/run',
     )
 
 
@@ -1759,9 +1743,6 @@ def test_create_archive_with_exit_codes_calls_borg_using_them():
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('expand_directories').and_return(())
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_runtime_directory'
-    ).and_return('/var/run/0/borgmatic')
     flexmock(module).should_receive('collect_borgmatic_runtime_directories').and_return([])
     flexmock(module).should_receive('make_base_create_command').and_return(
         (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
@@ -1791,6 +1772,7 @@ def test_create_archive_with_exit_codes_calls_borg_using_them():
         config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
         global_arguments=flexmock(log_json=False),
+        borgmatic_runtime_directory='/borgmatic/run',
     )
 
 
@@ -1798,9 +1780,6 @@ def test_create_archive_with_stats_calls_borg_with_stats_parameter_and_answer_ou
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('expand_directories').and_return(())
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_runtime_directory'
-    ).and_return('/var/run/0/borgmatic')
     flexmock(module).should_receive('collect_borgmatic_runtime_directories').and_return([])
     flexmock(module).should_receive('make_base_create_command').and_return(
         (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
@@ -1828,6 +1807,7 @@ def test_create_archive_with_stats_calls_borg_with_stats_parameter_and_answer_ou
         config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
         global_arguments=flexmock(log_json=False),
+        borgmatic_runtime_directory='/borgmatic/run',
         stats=True,
     )
 
@@ -1836,9 +1816,6 @@ def test_create_archive_with_files_calls_borg_with_answer_output_log_level():
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('expand_directories').and_return(())
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_runtime_directory'
-    ).and_return('/var/run/0/borgmatic')
     flexmock(module).should_receive('collect_borgmatic_runtime_directories').and_return([])
     flexmock(module).should_receive('make_base_create_command').and_return(
         (
@@ -1871,6 +1848,7 @@ def test_create_archive_with_files_calls_borg_with_answer_output_log_level():
         config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
         global_arguments=flexmock(log_json=False),
+        borgmatic_runtime_directory='/borgmatic/run',
         list_files=True,
     )
 
@@ -1879,9 +1857,6 @@ def test_create_archive_with_progress_and_log_info_calls_borg_with_progress_para
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('expand_directories').and_return(())
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_runtime_directory'
-    ).and_return('/var/run/0/borgmatic')
     flexmock(module).should_receive('collect_borgmatic_runtime_directories').and_return([])
     flexmock(module).should_receive('make_base_create_command').and_return(
         (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
@@ -1910,6 +1885,7 @@ def test_create_archive_with_progress_and_log_info_calls_borg_with_progress_para
         config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
         global_arguments=flexmock(log_json=False),
+        borgmatic_runtime_directory='/borgmatic/run',
         progress=True,
     )
 
@@ -1918,9 +1894,6 @@ def test_create_archive_with_progress_calls_borg_with_progress_parameter():
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('expand_directories').and_return(())
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_runtime_directory'
-    ).and_return('/var/run/0/borgmatic')
     flexmock(module).should_receive('collect_borgmatic_runtime_directories').and_return([])
     flexmock(module).should_receive('make_base_create_command').and_return(
         (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
@@ -1948,6 +1921,7 @@ def test_create_archive_with_progress_calls_borg_with_progress_parameter():
         config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
         global_arguments=flexmock(log_json=False),
+        borgmatic_runtime_directory='/borgmatic/run',
         progress=True,
     )
 
@@ -1957,9 +1931,6 @@ def test_create_archive_with_progress_and_stream_processes_calls_borg_with_progr
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     processes = flexmock()
     flexmock(module).should_receive('expand_directories').and_return(())
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_runtime_directory'
-    ).and_return('/var/run/0/borgmatic')
     flexmock(module).should_receive('collect_borgmatic_runtime_directories').and_return([])
     flexmock(module).should_receive('make_base_create_command').and_return(
         (
@@ -2009,6 +1980,7 @@ def test_create_archive_with_progress_and_stream_processes_calls_borg_with_progr
         config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
         global_arguments=flexmock(log_json=False),
+        borgmatic_runtime_directory='/borgmatic/run',
         progress=True,
         stream_processes=processes,
     )
@@ -2018,9 +1990,6 @@ def test_create_archive_with_json_calls_borg_with_json_flag():
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('expand_directories').and_return(())
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_runtime_directory'
-    ).and_return('/var/run/0/borgmatic')
     flexmock(module).should_receive('collect_borgmatic_runtime_directories').and_return([])
     flexmock(module).should_receive('make_base_create_command').and_return(
         (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
@@ -2046,6 +2015,7 @@ def test_create_archive_with_json_calls_borg_with_json_flag():
         config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
         global_arguments=flexmock(log_json=False),
+        borgmatic_runtime_directory='/borgmatic/run',
         json=True,
     )
 
@@ -2056,9 +2026,6 @@ def test_create_archive_with_stats_and_json_calls_borg_without_stats_flag():
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('expand_directories').and_return(())
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_runtime_directory'
-    ).and_return('/var/run/0/borgmatic')
     flexmock(module).should_receive('collect_borgmatic_runtime_directories').and_return([])
     flexmock(module).should_receive('make_base_create_command').and_return(
         (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
@@ -2084,6 +2051,7 @@ def test_create_archive_with_stats_and_json_calls_borg_without_stats_flag():
         config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
         global_arguments=flexmock(log_json=False),
+        borgmatic_runtime_directory='/borgmatic/run',
         json=True,
         stats=True,
     )
@@ -2095,9 +2063,6 @@ def test_create_archive_calls_borg_with_working_directory():
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module).should_receive('expand_directories').and_return(())
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_runtime_directory'
-    ).and_return('/var/run/0/borgmatic')
     flexmock(module).should_receive('collect_borgmatic_runtime_directories').and_return([])
     flexmock(module).should_receive('make_base_create_command').and_return(
         (('borg', 'create'), REPO_ARCHIVE_WITH_PATHS, flexmock(), flexmock())
@@ -2128,6 +2093,7 @@ def test_create_archive_calls_borg_with_working_directory():
         config_paths=['/tmp/test.yaml'],
         local_borg_version='1.2.3',
         global_arguments=flexmock(log_json=False),
+        borgmatic_runtime_directory='/borgmatic/run',
     )
 
 

+ 100 - 25
tests/unit/config/test_paths.py

@@ -33,53 +33,120 @@ def test_get_borgmatic_source_directory_without_config_option_uses_default():
     assert module.get_borgmatic_source_directory({}) == '~/.borgmatic'
 
 
-def test_get_borgmatic_runtime_directory_uses_config_option():
+def test_runtime_directory_uses_config_option():
     flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
+    config = {'user_runtime_directory': '/run', 'borgmatic_source_directory': '/nope'}
 
-    assert (
-        module.get_borgmatic_runtime_directory(
-            {'user_runtime_directory': '/tmp', 'borgmatic_source_directory': '/nope'}
-        )
-        == '/tmp/./borgmatic'
+    with module.Runtime_directory(config) as borgmatic_runtime_directory:
+        assert borgmatic_runtime_directory == '/run/./borgmatic'
+
+
+def test_runtime_directory_uses_config_option_without_adding_duplicate_borgmatic_subdirectory():
+    flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
+    config = {'user_runtime_directory': '/run/borgmatic', 'borgmatic_source_directory': '/nope'}
+
+    with module.Runtime_directory(config) as borgmatic_runtime_directory:
+        assert borgmatic_runtime_directory == '/run/./borgmatic'
+
+
+def test_runtime_directory_falls_back_to_xdg_runtime_dir():
+    flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
+    flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(
+        '/run'
     )
 
+    with module.Runtime_directory({}) as borgmatic_runtime_directory:
+        assert borgmatic_runtime_directory == '/run/./borgmatic'
+
 
-def test_get_borgmatic_runtime_directory_falls_back_to_linux_environment_variable():
+def test_runtime_directory_falls_back_to_xdg_runtime_dir_without_adding_duplicate_borgmatic_subdirectory():
     flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
     flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(
-        '/tmp'
+        '/run/borgmatic'
+    )
+
+    with module.Runtime_directory({}) as borgmatic_runtime_directory:
+        assert borgmatic_runtime_directory == '/run/./borgmatic'
+
+
+def test_runtime_directory_falls_back_to_runtime_directory():
+    flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
+    flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
+    flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
+        '/run'
     )
 
-    assert module.get_borgmatic_runtime_directory({}) == '/tmp/./borgmatic'
+    with module.Runtime_directory({}) as borgmatic_runtime_directory:
+        assert borgmatic_runtime_directory == '/run/./borgmatic'
 
 
-def test_get_borgmatic_runtime_directory_falls_back_to_macos_environment_variable():
+def test_runtime_directory_falls_back_to_runtime_directory_without_adding_duplicate_borgmatic_subdirectory():
     flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
     flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
-    flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return('/tmp')
+    flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
+        '/run/borgmatic'
+    )
 
-    assert module.get_borgmatic_runtime_directory({}) == '/tmp/./borgmatic'
+    with module.Runtime_directory({}) as borgmatic_runtime_directory:
+        assert borgmatic_runtime_directory == '/run/./borgmatic'
 
 
-def test_get_borgmatic_runtime_directory_falls_back_to_other_environment_variable():
+def test_runtime_directory_falls_back_to_tmpdir_and_adds_temporary_subdirectory_that_get_cleaned_up():
     flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
     flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
+    flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
+        None
+    )
+    flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return('/run')
+    temporary_directory = flexmock(name='/run/borgmatic-1234')
+    temporary_directory.should_receive('cleanup').once()
+    flexmock(module.tempfile).should_receive('TemporaryDirectory').with_args(
+        prefix='borgmatic', dir='/run'
+    ).and_return(temporary_directory)
+
+    with module.Runtime_directory({}) as borgmatic_runtime_directory:
+        assert borgmatic_runtime_directory == '/run/borgmatic-1234/./borgmatic'
+
+
+def test_runtime_directory_falls_back_to_temp_and_adds_temporary_subdirectory_that_get_cleaned_up():
+    flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
+    flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
+    flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
+        None
+    )
     flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return(None)
-    flexmock(module.os.environ).should_receive('get').with_args('TEMP').and_return('/tmp')
+    flexmock(module.os.environ).should_receive('get').with_args('TEMP').and_return('/run')
+    temporary_directory = flexmock(name='/run/borgmatic-1234')
+    temporary_directory.should_receive('cleanup').once()
+    flexmock(module.tempfile).should_receive('TemporaryDirectory').with_args(
+        prefix='borgmatic', dir='/run'
+    ).and_return(temporary_directory)
 
-    assert module.get_borgmatic_runtime_directory({}) == '/tmp/./borgmatic'
+    with module.Runtime_directory({}) as borgmatic_runtime_directory:
+        assert borgmatic_runtime_directory == '/run/borgmatic-1234/./borgmatic'
 
 
-def test_get_borgmatic_runtime_directory_defaults_to_hard_coded_path():
+def test_runtime_directory_falls_back_to_hard_coded_tmp_path_and_adds_temporary_subdirectory_that_get_cleaned_up():
     flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
-    flexmock(module.os.environ).should_receive('get').and_return('/run/user/0')
-    flexmock(module.os).should_receive('getuid').and_return(0)
+    flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
+    flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
+        None
+    )
+    flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return(None)
+    flexmock(module.os.environ).should_receive('get').with_args('TEMP').and_return(None)
+    temporary_directory = flexmock(name='/tmp/borgmatic-1234')
+    temporary_directory.should_receive('cleanup').once()
+    flexmock(module.tempfile).should_receive('TemporaryDirectory').with_args(
+        prefix='borgmatic', dir='/tmp'
+    ).and_return(temporary_directory)
 
-    assert module.get_borgmatic_runtime_directory({}) == '/run/user/0/./borgmatic'
+    with module.Runtime_directory({}) as borgmatic_runtime_directory:
+        assert borgmatic_runtime_directory == '/tmp/borgmatic-1234/./borgmatic'
 
 
 def test_get_borgmatic_state_directory_uses_config_option():
     flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
+    flexmock(module.os.environ).should_receive('get').never()
 
     assert (
         module.get_borgmatic_state_directory(
@@ -89,16 +156,24 @@ def test_get_borgmatic_state_directory_uses_config_option():
     )
 
 
-def test_get_borgmatic_state_directory_falls_back_to_environment_variable():
+def test_get_borgmatic_state_directory_falls_back_to_xdg_state_home():
     flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
-    flexmock(module.os.environ).should_receive('get').with_args(
-        'XDG_STATE_HOME', object
-    ).and_return('/tmp')
+    flexmock(module.os.environ).should_receive('get').with_args('XDG_STATE_HOME').and_return('/tmp')
+
+    assert module.get_borgmatic_state_directory({}) == '/tmp/borgmatic'
+
+
+def test_get_borgmatic_state_directory_falls_back_to_state_directory():
+    flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
+    flexmock(module.os.environ).should_receive('get').with_args('XDG_STATE_HOME').and_return(None)
+    flexmock(module.os.environ).should_receive('get').with_args('STATE_DIRECTORY').and_return(
+        '/tmp'
+    )
 
     assert module.get_borgmatic_state_directory({}) == '/tmp/borgmatic'
 
 
 def test_get_borgmatic_state_directory_defaults_to_hard_coded_path():
     flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
-    flexmock(module.os.environ).should_receive('get').and_return('/root/.local/state')
-    assert module.get_borgmatic_state_directory({}) == '/root/.local/state/borgmatic'
+    flexmock(module.os.environ).should_receive('get').and_return(None)
+    assert module.get_borgmatic_state_directory({}) == '~/.local/state/borgmatic'

+ 27 - 6
tests/unit/hooks/test_mariadb.py

@@ -73,7 +73,12 @@ def test_dump_data_sources_dumps_each_database():
             dry_run_label=object,
         ).and_return(process).once()
 
-    assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == processes
+    assert (
+        module.dump_data_sources(
+            databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False
+        )
+        == processes
+    )
 
 
 def test_dump_data_sources_dumps_with_password():
@@ -94,7 +99,9 @@ def test_dump_data_sources_dumps_with_password():
         dry_run_label=object,
     ).and_return(process).once()
 
-    assert module.dump_data_sources([database], {}, 'test.yaml', dry_run=False) == [process]
+    assert module.dump_data_sources(
+        [database], {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False
+    ) == [process]
 
 
 def test_dump_data_sources_dumps_all_databases_at_once():
@@ -112,7 +119,9 @@ def test_dump_data_sources_dumps_all_databases_at_once():
         dry_run_label=object,
     ).and_return(process).once()
 
-    assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process]
+    assert module.dump_data_sources(
+        databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False
+    ) == [process]
 
 
 def test_dump_data_sources_dumps_all_databases_separately_when_format_configured():
@@ -132,7 +141,12 @@ def test_dump_data_sources_dumps_all_databases_separately_when_format_configured
             dry_run_label=object,
         ).and_return(process).once()
 
-    assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == processes
+    assert (
+        module.dump_data_sources(
+            databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False
+        )
+        == processes
+    )
 
 
 def test_database_names_to_dump_runs_mariadb_with_list_options():
@@ -434,7 +448,9 @@ def test_dump_data_sources_errors_for_missing_all_databases():
     flexmock(module).should_receive('database_names_to_dump').and_return(())
 
     with pytest.raises(ValueError):
-        assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False)
+        assert module.dump_data_sources(
+            databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False
+        )
 
 
 def test_dump_data_sources_does_not_error_for_missing_all_databases_with_dry_run():
@@ -445,7 +461,12 @@ def test_dump_data_sources_does_not_error_for_missing_all_databases_with_dry_run
     )
     flexmock(module).should_receive('database_names_to_dump').and_return(())
 
-    assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=True) == []
+    assert (
+        module.dump_data_sources(
+            databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=True
+        )
+        == []
+    )
 
 
 def test_restore_data_source_dump_runs_mariadb_to_restore():

+ 30 - 7
tests/unit/hooks/test_mongodb.py

@@ -41,7 +41,12 @@ def test_dump_data_sources_runs_mongodump_for_each_database():
             run_to_completion=False,
         ).and_return(process).once()
 
-    assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == processes
+    assert (
+        module.dump_data_sources(
+            databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False
+        )
+        == processes
+    )
 
 
 def test_dump_data_sources_with_dry_run_skips_mongodump():
@@ -53,7 +58,12 @@ def test_dump_data_sources_with_dry_run_skips_mongodump():
     flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
     flexmock(module).should_receive('execute_command').never()
 
-    assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=True) == []
+    assert (
+        module.dump_data_sources(
+            databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=True
+        )
+        == []
+    )
 
 
 def test_dump_data_sources_runs_mongodump_with_hostname_and_port():
@@ -82,7 +92,9 @@ def test_dump_data_sources_runs_mongodump_with_hostname_and_port():
         run_to_completion=False,
     ).and_return(process).once()
 
-    assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process]
+    assert module.dump_data_sources(
+        databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False
+    ) == [process]
 
 
 def test_dump_data_sources_runs_mongodump_with_username_and_password():
@@ -120,7 +132,9 @@ def test_dump_data_sources_runs_mongodump_with_username_and_password():
         run_to_completion=False,
     ).and_return(process).once()
 
-    assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process]
+    assert module.dump_data_sources(
+        databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False
+    ) == [process]
 
 
 def test_dump_data_sources_runs_mongodump_with_directory_format():
@@ -137,7 +151,12 @@ def test_dump_data_sources_runs_mongodump_with_directory_format():
         shell=True,
     ).and_return(flexmock()).once()
 
-    assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == []
+    assert (
+        module.dump_data_sources(
+            databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False
+        )
+        == []
+    )
 
 
 def test_dump_data_sources_runs_mongodump_with_options():
@@ -163,7 +182,9 @@ def test_dump_data_sources_runs_mongodump_with_options():
         run_to_completion=False,
     ).and_return(process).once()
 
-    assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process]
+    assert module.dump_data_sources(
+        databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False
+    ) == [process]
 
 
 def test_dump_data_sources_runs_mongodumpall_for_all_databases():
@@ -181,7 +202,9 @@ def test_dump_data_sources_runs_mongodumpall_for_all_databases():
         run_to_completion=False,
     ).and_return(process).once()
 
-    assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process]
+    assert module.dump_data_sources(
+        databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False
+    ) == [process]
 
 
 def test_build_dump_command_with_username_injection_attack_gets_escaped():

+ 27 - 6
tests/unit/hooks/test_mysql.py

@@ -73,7 +73,12 @@ def test_dump_data_sources_dumps_each_database():
             dry_run_label=object,
         ).and_return(process).once()
 
-    assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == processes
+    assert (
+        module.dump_data_sources(
+            databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False
+        )
+        == processes
+    )
 
 
 def test_dump_data_sources_dumps_with_password():
@@ -94,7 +99,9 @@ def test_dump_data_sources_dumps_with_password():
         dry_run_label=object,
     ).and_return(process).once()
 
-    assert module.dump_data_sources([database], {}, 'test.yaml', dry_run=False) == [process]
+    assert module.dump_data_sources(
+        [database], {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False
+    ) == [process]
 
 
 def test_dump_data_sources_dumps_all_databases_at_once():
@@ -112,7 +119,9 @@ def test_dump_data_sources_dumps_all_databases_at_once():
         dry_run_label=object,
     ).and_return(process).once()
 
-    assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process]
+    assert module.dump_data_sources(
+        databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False
+    ) == [process]
 
 
 def test_dump_data_sources_dumps_all_databases_separately_when_format_configured():
@@ -132,7 +141,12 @@ def test_dump_data_sources_dumps_all_databases_separately_when_format_configured
             dry_run_label=object,
         ).and_return(process).once()
 
-    assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == processes
+    assert (
+        module.dump_data_sources(
+            databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False
+        )
+        == processes
+    )
 
 
 def test_database_names_to_dump_runs_mysql_with_list_options():
@@ -432,7 +446,9 @@ def test_dump_data_sources_errors_for_missing_all_databases():
     flexmock(module).should_receive('database_names_to_dump').and_return(())
 
     with pytest.raises(ValueError):
-        assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False)
+        assert module.dump_data_sources(
+            databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False
+        )
 
 
 def test_dump_data_sources_does_not_error_for_missing_all_databases_with_dry_run():
@@ -443,7 +459,12 @@ def test_dump_data_sources_does_not_error_for_missing_all_databases_with_dry_run
     )
     flexmock(module).should_receive('database_names_to_dump').and_return(())
 
-    assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=True) == []
+    assert (
+        module.dump_data_sources(
+            databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=True
+        )
+        == []
+    )
 
 
 def test_restore_data_source_dump_runs_mysql_to_restore():

+ 48 - 12
tests/unit/hooks/test_postgresql.py

@@ -251,7 +251,12 @@ def test_dump_data_sources_runs_pg_dump_for_each_database():
             run_to_completion=False,
         ).and_return(process).once()
 
-    assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == processes
+    assert (
+        module.dump_data_sources(
+            databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False
+        )
+        == processes
+    )
 
 
 def test_dump_data_sources_raises_when_no_database_names_to_dump():
@@ -261,7 +266,9 @@ def test_dump_data_sources_raises_when_no_database_names_to_dump():
     flexmock(module).should_receive('database_names_to_dump').and_return(())
 
     with pytest.raises(ValueError):
-        module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False)
+        module.dump_data_sources(
+            databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False
+        )
 
 
 def test_dump_data_sources_does_not_raise_when_no_database_names_to_dump():
@@ -270,7 +277,9 @@ def test_dump_data_sources_does_not_raise_when_no_database_names_to_dump():
     flexmock(module).should_receive('make_dump_path').and_return('')
     flexmock(module).should_receive('database_names_to_dump').and_return(())
 
-    module.dump_data_sources(databases, {}, 'test.yaml', dry_run=True) == []
+    module.dump_data_sources(
+        databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=True
+    ) == []
 
 
 def test_dump_data_sources_with_duplicate_dump_skips_pg_dump():
@@ -287,7 +296,12 @@ def test_dump_data_sources_with_duplicate_dump_skips_pg_dump():
     flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
     flexmock(module).should_receive('execute_command').never()
 
-    assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == []
+    assert (
+        module.dump_data_sources(
+            databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False
+        )
+        == []
+    )
 
 
 def test_dump_data_sources_with_dry_run_skips_pg_dump():
@@ -304,7 +318,12 @@ def test_dump_data_sources_with_dry_run_skips_pg_dump():
     flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
     flexmock(module).should_receive('execute_command').never()
 
-    assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=True) == []
+    assert (
+        module.dump_data_sources(
+            databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=True
+        )
+        == []
+    )
 
 
 def test_dump_data_sources_runs_pg_dump_with_hostname_and_port():
@@ -340,7 +359,9 @@ def test_dump_data_sources_runs_pg_dump_with_hostname_and_port():
         run_to_completion=False,
     ).and_return(process).once()
 
-    assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process]
+    assert module.dump_data_sources(
+        databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False
+    ) == [process]
 
 
 def test_dump_data_sources_runs_pg_dump_with_username_and_password():
@@ -376,7 +397,9 @@ def test_dump_data_sources_runs_pg_dump_with_username_and_password():
         run_to_completion=False,
     ).and_return(process).once()
 
-    assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process]
+    assert module.dump_data_sources(
+        databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False
+    ) == [process]
 
 
 def test_dump_data_sources_with_username_injection_attack_gets_escaped():
@@ -412,7 +435,9 @@ def test_dump_data_sources_with_username_injection_attack_gets_escaped():
         run_to_completion=False,
     ).and_return(process).once()
 
-    assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process]
+    assert module.dump_data_sources(
+        databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False
+    ) == [process]
 
 
 def test_dump_data_sources_runs_pg_dump_with_directory_format():
@@ -443,7 +468,12 @@ def test_dump_data_sources_runs_pg_dump_with_directory_format():
         extra_environment={'PGSSLMODE': 'disable'},
     ).and_return(flexmock()).once()
 
-    assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == []
+    assert (
+        module.dump_data_sources(
+            databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False
+        )
+        == []
+    )
 
 
 def test_dump_data_sources_runs_pg_dump_with_options():
@@ -476,7 +506,9 @@ def test_dump_data_sources_runs_pg_dump_with_options():
         run_to_completion=False,
     ).and_return(process).once()
 
-    assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process]
+    assert module.dump_data_sources(
+        databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False
+    ) == [process]
 
 
 def test_dump_data_sources_runs_pg_dumpall_for_all_databases():
@@ -498,7 +530,9 @@ def test_dump_data_sources_runs_pg_dumpall_for_all_databases():
         run_to_completion=False,
     ).and_return(process).once()
 
-    assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process]
+    assert module.dump_data_sources(
+        databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False
+    ) == [process]
 
 
 def test_dump_data_sources_runs_non_default_pg_dump():
@@ -532,7 +566,9 @@ def test_dump_data_sources_runs_non_default_pg_dump():
         run_to_completion=False,
     ).and_return(process).once()
 
-    assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process]
+    assert module.dump_data_sources(
+        databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False
+    ) == [process]
 
 
 def test_restore_data_source_dump_runs_pg_restore():

+ 49 - 19
tests/unit/hooks/test_sqlite.py

@@ -18,15 +18,20 @@ def test_use_streaming_false_for_no_databases():
 def test_dump_data_sources_logs_and_skips_if_dump_already_exists():
     databases = [{'path': '/path/to/database', 'name': 'database'}]
 
-    flexmock(module).should_receive('make_dump_path').and_return('/path/to/dump')
+    flexmock(module).should_receive('make_dump_path').and_return('/run/borgmatic')
     flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
-        '/path/to/dump/database'
+        '/run/borgmatic/database'
     )
     flexmock(module.os.path).should_receive('exists').and_return(True)
     flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
     flexmock(module).should_receive('execute_command').never()
 
-    assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == []
+    assert (
+        module.dump_data_sources(
+            databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False
+        )
+        == []
+    )
 
 
 def test_dump_data_sources_dumps_each_database():
@@ -36,9 +41,9 @@ def test_dump_data_sources_dumps_each_database():
     ]
     processes = [flexmock(), flexmock()]
 
-    flexmock(module).should_receive('make_dump_path').and_return('/path/to/dump')
+    flexmock(module).should_receive('make_dump_path').and_return('/run/borgmatic')
     flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
-        '/path/to/dump/database'
+        '/run/borgmatic/database'
     )
     flexmock(module.os.path).should_receive('exists').and_return(False)
     flexmock(module.dump).should_receive('create_named_pipe_for_dump')
@@ -46,7 +51,12 @@ def test_dump_data_sources_dumps_each_database():
         processes[1]
     )
 
-    assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == processes
+    assert (
+        module.dump_data_sources(
+            databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False
+        )
+        == processes
+    )
 
 
 def test_dump_data_sources_with_path_injection_attack_gets_escaped():
@@ -55,9 +65,9 @@ def test_dump_data_sources_with_path_injection_attack_gets_escaped():
     ]
     processes = [flexmock()]
 
-    flexmock(module).should_receive('make_dump_path').and_return('/path/to/dump')
+    flexmock(module).should_receive('make_dump_path').and_return('/run/borgmatic')
     flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
-        '/path/to/dump/database'
+        '/run/borgmatic/database'
     )
     flexmock(module.os.path).should_receive('exists').and_return(False)
     flexmock(module.dump).should_receive('create_named_pipe_for_dump')
@@ -67,13 +77,18 @@ def test_dump_data_sources_with_path_injection_attack_gets_escaped():
             "'/path/to/database1; naughty-command'",
             '.dump',
             '>',
-            '/path/to/dump/database',
+            '/run/borgmatic/database',
         ),
         shell=True,
         run_to_completion=False,
     ).and_return(processes[0])
 
-    assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == processes
+    assert (
+        module.dump_data_sources(
+            databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False
+        )
+        == processes
+    )
 
 
 def test_dump_data_sources_with_non_existent_path_warns_and_dumps_database():
@@ -82,16 +97,21 @@ def test_dump_data_sources_with_non_existent_path_warns_and_dumps_database():
     ]
     processes = [flexmock()]
 
-    flexmock(module).should_receive('make_dump_path').and_return('/path/to/dump')
+    flexmock(module).should_receive('make_dump_path').and_return('/run/borgmatic')
     flexmock(module.logger).should_receive('warning').once()
     flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
-        '/path/to/dump/database'
+        '/run/borgmatic'
     )
     flexmock(module.os.path).should_receive('exists').and_return(False)
     flexmock(module.dump).should_receive('create_named_pipe_for_dump')
     flexmock(module).should_receive('execute_command').and_return(processes[0])
 
-    assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == processes
+    assert (
+        module.dump_data_sources(
+            databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False
+        )
+        == processes
+    )
 
 
 def test_dump_data_sources_with_name_all_warns_and_dumps_all_databases():
@@ -100,32 +120,42 @@ def test_dump_data_sources_with_name_all_warns_and_dumps_all_databases():
     ]
     processes = [flexmock()]
 
-    flexmock(module).should_receive('make_dump_path').and_return('/path/to/dump')
+    flexmock(module).should_receive('make_dump_path').and_return('/run/borgmatic')
     flexmock(module.logger).should_receive(
         'warning'
     ).twice()  # once for the name=all, once for the non-existent path
     flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
-        '/path/to/dump/database'
+        '/run/borgmatic/database'
     )
     flexmock(module.os.path).should_receive('exists').and_return(False)
     flexmock(module.dump).should_receive('create_named_pipe_for_dump')
     flexmock(module).should_receive('execute_command').and_return(processes[0])
 
-    assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == processes
+    assert (
+        module.dump_data_sources(
+            databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=False
+        )
+        == processes
+    )
 
 
 def test_dump_data_sources_does_not_dump_if_dry_run():
     databases = [{'path': '/path/to/database', 'name': 'database'}]
 
-    flexmock(module).should_receive('make_dump_path').and_return('/path/to/dump')
+    flexmock(module).should_receive('make_dump_path').and_return('/run/borgmatic')
     flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
-        '/path/to/dump/database'
+        '/run/borgmatic'
     )
     flexmock(module.os.path).should_receive('exists').and_return(False)
     flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
     flexmock(module).should_receive('execute_command').never()
 
-    assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=True) == []
+    assert (
+        module.dump_data_sources(
+            databases, {}, 'test.yaml', borgmatic_runtime_directory='/run/borgmatic', dry_run=True
+        )
+        == []
+    )
 
 
 def test_restore_data_source_dump_restores_database():