From 68864395b55217590d98cf531745b0bab62905f9 Mon Sep 17 00:00:00 2001 From: Simon Pilkington Date: Thu, 23 Oct 2025 14:24:53 +0200 Subject: [PATCH] Support multiple configured systemd service directories (RuntimeDirectory, StateDirectory) --- borgmatic/config/paths.py | 28 ++++++++++- tests/unit/config/test_paths.py | 89 +++++++++++++++++++++++++++++---- 2 files changed, 105 insertions(+), 12 deletions(-) diff --git a/borgmatic/config/paths.py b/borgmatic/config/paths.py index 8ce930cd..1788d3e2 100644 --- a/borgmatic/config/paths.py +++ b/borgmatic/config/paths.py @@ -2,10 +2,19 @@ import contextlib import logging import os import tempfile +from enum import Enum logger = logging.getLogger(__name__) +class Systemd_directories(Enum): + RUNTIME_DIRECTORY = 0 + STATE_DIRECTORY = 1 + CACHE_DIRECTORY = 2 + LOGS_DIRECTORY = 3 + CONFIGURATION_DIRECTORY = 4 + + def expand_user_in_path(path): ''' Given a directory path, expand any tildes in it. @@ -16,6 +25,17 @@ def expand_user_in_path(path): return None +def resolve_systemd_directory(directory): + ''' + Given a systemd directory environment variable enum, read the value if set and return the first + configured directory. + ''' + separator = ':' + + paths = os.environ.get(directory.name) + return paths.split(separator)[0] if paths else None + + def get_working_directory(config): # pragma: no cover ''' Given a configuration dict, get the working directory from it, expanding any tildes. @@ -96,7 +116,9 @@ class Runtime_directory: runtime_directory = ( config.get('user_runtime_directory') or os.environ.get('XDG_RUNTIME_DIR') # Set by PAM on Linux. - or os.environ.get('RUNTIME_DIRECTORY') # Set by systemd if configured. + or resolve_systemd_directory( + Systemd_directories.RUNTIME_DIRECTORY + ) # Set by systemd if configured. ) if runtime_directory: @@ -174,7 +196,9 @@ def get_borgmatic_state_directory(config): os.path.join( config.get('user_state_directory') or os.environ.get('XDG_STATE_HOME') - or os.environ.get('STATE_DIRECTORY') # Set by systemd if configured. + or resolve_systemd_directory( + Systemd_directories.STATE_DIRECTORY + ) # Set by systemd if configured. or '~/.local/state', 'borgmatic', ), diff --git a/tests/unit/config/test_paths.py b/tests/unit/config/test_paths.py index 76cf7248..eb2d3790 100644 --- a/tests/unit/config/test_paths.py +++ b/tests/unit/config/test_paths.py @@ -121,7 +121,9 @@ def test_runtime_directory_with_relative_xdg_runtime_dir_errors(): 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( + flexmock(module).should_receive('resolve_systemd_directory').with_args( + module.Systemd_directories.RUNTIME_DIRECTORY + ).and_return( '/run', ) flexmock(module.os).should_receive('makedirs') @@ -133,7 +135,9 @@ def test_runtime_directory_falls_back_to_runtime_directory(): 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('RUNTIME_DIRECTORY').and_return( + flexmock(module).should_receive('resolve_systemd_directory').with_args( + module.Systemd_directories.RUNTIME_DIRECTORY + ).and_return( '/run/borgmatic', ) flexmock(module.os).should_receive('makedirs') @@ -144,7 +148,9 @@ def test_runtime_directory_falls_back_to_runtime_directory_without_adding_duplic def test_runtime_directory_with_relative_runtime_directory_errors(): 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( + flexmock(module).should_receive('resolve_systemd_directory').with_args( + module.Systemd_directories.RUNTIME_DIRECTORY + ).and_return( 'run', ) flexmock(module.os).should_receive('makedirs').never() @@ -156,7 +162,9 @@ def test_runtime_directory_with_relative_runtime_directory_errors(): 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( + flexmock(module).should_receive('resolve_systemd_directory').with_args( + module.Systemd_directories.RUNTIME_DIRECTORY + ).and_return( None, ) flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return('/run') @@ -174,7 +182,9 @@ def test_runtime_directory_falls_back_to_tmpdir_and_adds_temporary_subdirectory_ def test_runtime_directory_with_relative_tmpdir_errors(): 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( + flexmock(module).should_receive('resolve_systemd_directory').with_args( + module.Systemd_directories.RUNTIME_DIRECTORY + ).and_return( None, ) flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return('run') @@ -188,7 +198,9 @@ def test_runtime_directory_with_relative_tmpdir_errors(): 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( + flexmock(module).should_receive('resolve_systemd_directory').with_args( + module.Systemd_directories.RUNTIME_DIRECTORY + ).and_return( None, ) flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return(None) @@ -207,7 +219,9 @@ def test_runtime_directory_falls_back_to_temp_and_adds_temporary_subdirectory_th def test_runtime_directory_with_relative_temp_errors(): 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( + flexmock(module).should_receive('resolve_systemd_directory').with_args( + module.Systemd_directories.RUNTIME_DIRECTORY + ).and_return( None, ) flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return(None) @@ -222,7 +236,9 @@ def test_runtime_directory_with_relative_temp_errors(): 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').with_args('XDG_RUNTIME_DIR').and_return(None) - flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return( + flexmock(module).should_receive('resolve_systemd_directory').with_args( + module.Systemd_directories.RUNTIME_DIRECTORY + ).and_return( None, ) flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return(None) @@ -242,7 +258,9 @@ def test_runtime_directory_falls_back_to_hard_coded_tmp_path_and_adds_temporary_ def test_runtime_directory_with_erroring_cleanup_does_not_raise(): 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( + flexmock(module).should_receive('resolve_systemd_directory').with_args( + module.Systemd_directories.RUNTIME_DIRECTORY + ).and_return( None, ) flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return(None) @@ -293,7 +311,9 @@ def test_get_borgmatic_state_directory_falls_back_to_xdg_state_home(): 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( + flexmock(module).should_receive('resolve_systemd_directory').with_args( + module.Systemd_directories.STATE_DIRECTORY + ).and_return( '/tmp', ) @@ -304,3 +324,52 @@ 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(None) assert module.get_borgmatic_state_directory({}) == '~/.local/state/borgmatic' + + +def test_resolve_systemd_directory_none(): + flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return( + None + ) + flexmock(module.os.environ).should_receive('get').with_args('STATE_DIRECTORY').and_return(None) + + assert module.resolve_systemd_directory(module.Systemd_directories.RUNTIME_DIRECTORY) is None + assert module.resolve_systemd_directory(module.Systemd_directories.STATE_DIRECTORY) is None + + +def test_resolve_systemd_directory_single(): + runtime_dir = '/run/borgmatic' + state_dir = '/var/lib/borgmatic' + + flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return( + runtime_dir + ) + flexmock(module.os.environ).should_receive('get').with_args('STATE_DIRECTORY').and_return( + state_dir + ) + + assert ( + module.resolve_systemd_directory(module.Systemd_directories.RUNTIME_DIRECTORY) + == runtime_dir + ) + assert module.resolve_systemd_directory(module.Systemd_directories.STATE_DIRECTORY) == state_dir + + +def test_resolve_systemd_directory_multiple(): + runtime_dirs = '/run/borgmatic:/run/second:/run/third' + state_dirs = '/var/lib/borgmatic:/var/lib/second:/var/lib/third' + + flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return( + runtime_dirs + ) + flexmock(module.os.environ).should_receive('get').with_args('STATE_DIRECTORY').and_return( + state_dirs + ) + + assert ( + module.resolve_systemd_directory(module.Systemd_directories.RUNTIME_DIRECTORY) + == '/run/borgmatic' + ) + assert ( + module.resolve_systemd_directory(module.Systemd_directories.STATE_DIRECTORY) + == '/var/lib/borgmatic' + )