Fix an error about the runtime directory getting excluded by tweaking its logic and lowering the error to a warning (#1211).

This commit is contained in:
Dan Helfman 2025-12-26 22:53:21 -08:00
commit ec8168af46
3 changed files with 97 additions and 44 deletions

2
NEWS
View file

@ -3,6 +3,8 @@
"--database" flag specifies.
* #1210: Fix an error when running the "spot" check or "extract" action with the "progress" option
or "--progress" flag.
* #1211: Fix an error about the runtime directory getting excluded by tweaking its logic and
lowering the error to a warning.
* #1212: Fix an error when restoring multiple directory-format database dumps at once.
2.0.13

View file

@ -96,22 +96,36 @@ def validate_planned_backup_paths(
if path_line and path_line.startswith(('- ', '+ '))
)
include_pattern_paths = {
pattern.path
for pattern in patterns
if pattern.type == borgmatic.borg.pattern.Pattern_type.INCLUDE
}
runtime_directory_root_patterns = tuple(
pattern
for pattern in patterns
if any_parent_directories(pattern.path, (borgmatic_runtime_directory,))
if pattern.type == borgmatic.borg.pattern.Pattern_type.ROOT
# Skip root patterns that have corresponding include patterns, because those will "punch
# through" any subsequent excludes.
if pattern.path not in include_pattern_paths
)
if not dry_run and os.path.exists(borgmatic_runtime_directory):
# If there are any root patterns in the runtime directory that are missing from the paths
# Borg is planning to backup, then they must've gotten excluded, e.g. by user-configured
# excludes. Error accordingly.
for pattern in runtime_directory_root_patterns:
if not any(any_parent_directories(path, (pattern.path,)) for path in paths):
raise ValueError(
f'The runtime directory {os.path.normpath(borgmatic_runtime_directory)} overlaps with the configured excludes or patterns with excludes. Please ensure the runtime directory is not excluded.',
)
# If all root patterns in the runtime directory are missing from the paths Borg is planning to
# backup, then they must've gotten excluded, e.g. by user-configured excludes. Warn accordingly.
if (
not dry_run
and os.path.exists(borgmatic_runtime_directory)
and runtime_directory_root_patterns
and not any(
any_parent_directories(path, (pattern.path,))
for pattern in runtime_directory_root_patterns
for path in paths
)
):
logger.warning(
f'The runtime directory {os.path.normpath(borgmatic_runtime_directory)} overlaps with the configured excludes (or the snapshotted source directories are empty). Please ensure the runtime directory is not excluded.'
)
# Return the subset of output paths *not* contained within the borgmatic runtime directory. The
# intent is that any downstream checks using these paths should skip runtime paths that

View file

@ -121,7 +121,7 @@ def test_validate_planned_backup_paths_skips_borgmatic_runtime_directory():
) == ('/foo', '/baz')
def test_validate_planned_backup_paths_with_borgmatic_runtime_directory_missing_from_paths_output_errors():
def test_validate_planned_backup_paths_with_borgmatic_runtime_directory_missing_from_paths_output_warns():
flexmock(module.flags).should_receive('omit_flag').replace_with(
lambda arguments, flag: arguments,
)
@ -136,26 +136,26 @@ def test_validate_planned_backup_paths_with_borgmatic_runtime_directory_missing_
flexmock(module).should_receive('any_parent_directories').replace_with(
lambda path, candidates: any(path.startswith(parent) for parent in candidates)
)
flexmock(module.logger).should_receive('warning').once()
with pytest.raises(ValueError):
module.validate_planned_backup_paths(
dry_run=False,
create_command=('borg', 'create'),
config={},
patterns=(
module.borgmatic.borg.pattern.Pattern('/foo'),
module.borgmatic.borg.pattern.Pattern(
'/run/borgmatic/bar', module.borgmatic.borg.pattern.Pattern_type.ROOT
),
module.borgmatic.borg.pattern.Pattern('/baz'),
assert module.validate_planned_backup_paths(
dry_run=False,
create_command=('borg', 'create'),
config={},
patterns=(
module.borgmatic.borg.pattern.Pattern('/foo'),
module.borgmatic.borg.pattern.Pattern(
'/run/borgmatic/bar', module.borgmatic.borg.pattern.Pattern_type.ROOT
),
local_path=None,
working_directory=None,
borgmatic_runtime_directory='/run/borgmatic',
)
module.borgmatic.borg.pattern.Pattern('/baz'),
),
local_path=None,
working_directory=None,
borgmatic_runtime_directory='/run/borgmatic',
) == ('/foo', '/bar', '/baz')
def test_validate_planned_backup_paths_with_borgmatic_runtime_directory_partially_excluded_from_paths_output_errors():
def test_validate_planned_backup_paths_with_borgmatic_runtime_directory_partially_excluded_from_paths_output_does_not_warn():
flexmock(module.flags).should_receive('omit_flag').replace_with(
lambda arguments, flag: arguments,
)
@ -172,26 +172,63 @@ def test_validate_planned_backup_paths_with_borgmatic_runtime_directory_partiall
flexmock(module).should_receive('any_parent_directories').replace_with(
lambda path, candidates: any(path.startswith(parent) for parent in candidates)
)
flexmock(module.logger).should_receive('warning').never()
with pytest.raises(ValueError):
module.validate_planned_backup_paths(
dry_run=False,
create_command=('borg', 'create'),
config={},
patterns=(
module.borgmatic.borg.pattern.Pattern('/foo'),
module.borgmatic.borg.pattern.Pattern(
'/run/borgmatic/bar', module.borgmatic.borg.pattern.Pattern_type.ROOT
),
module.borgmatic.borg.pattern.Pattern('/baz'),
module.borgmatic.borg.pattern.Pattern(
'/run/borgmatic/quux', module.borgmatic.borg.pattern.Pattern_type.ROOT
),
assert module.validate_planned_backup_paths(
dry_run=False,
create_command=('borg', 'create'),
config={},
patterns=(
module.borgmatic.borg.pattern.Pattern('/foo'),
module.borgmatic.borg.pattern.Pattern(
'/run/borgmatic/bar', module.borgmatic.borg.pattern.Pattern_type.ROOT
),
local_path=None,
working_directory=None,
borgmatic_runtime_directory='/run/borgmatic',
)
module.borgmatic.borg.pattern.Pattern('/baz'),
module.borgmatic.borg.pattern.Pattern(
'/run/borgmatic/quux', module.borgmatic.borg.pattern.Pattern_type.ROOT
),
),
local_path=None,
working_directory=None,
borgmatic_runtime_directory='/run/borgmatic',
) == ('/foo', '/baz')
def test_validate_planned_backup_paths_with_borgmatic_runtime_directory_with_corresponding_include_and_missing_from_paths_output_does_not_warn():
flexmock(module.flags).should_receive('omit_flag').replace_with(
lambda arguments, flag: arguments,
)
flexmock(module.flags).should_receive('omit_flag_and_value').replace_with(
lambda arguments, flag: arguments,
)
flexmock(module.environment).should_receive('make_environment').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').and_return(
'+ /foo\n- /bar\n- /baz',
)
flexmock(module.os.path).should_receive('exists').and_return(True)
flexmock(module).should_receive('any_parent_directories').replace_with(
lambda path, candidates: any(path.startswith(parent) for parent in candidates)
)
flexmock(module.logger).should_receive('warning').never()
assert module.validate_planned_backup_paths(
dry_run=False,
create_command=('borg', 'create'),
config={},
patterns=(
module.borgmatic.borg.pattern.Pattern('/foo'),
module.borgmatic.borg.pattern.Pattern(
'/run/borgmatic/bar', module.borgmatic.borg.pattern.Pattern_type.ROOT
),
module.borgmatic.borg.pattern.Pattern(
'/run/borgmatic/bar', module.borgmatic.borg.pattern.Pattern_type.INCLUDE
),
module.borgmatic.borg.pattern.Pattern('/baz'),
),
local_path=None,
working_directory=None,
borgmatic_runtime_directory='/run/borgmatic',
) == ('/foo', '/bar', '/baz')
def test_validate_planned_backup_paths_with_borgmatic_runtime_directory_missing_from_patterns_does_not_raise():