Support multiple configured systemd service directories (RuntimeDirectory, StateDirectory)
All checks were successful
build / test (pull_request) Successful in 8m27s
build / docs (pull_request) Has been skipped

This commit is contained in:
Simon Pilkington 2025-10-23 14:24:53 +02:00
commit 68864395b5
2 changed files with 105 additions and 12 deletions

View file

@ -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',
),

View file

@ -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'
)