From ec8168af46d50a607b007d1c15f2e31d84fd68db Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Fri, 26 Dec 2025 22:53:21 -0800 Subject: [PATCH] Fix an error about the runtime directory getting excluded by tweaking its logic and lowering the error to a warning (#1211). --- NEWS | 2 + borgmatic/borg/create.py | 32 +++++++--- tests/unit/borg/test_create.py | 107 ++++++++++++++++++++++----------- 3 files changed, 97 insertions(+), 44 deletions(-) diff --git a/NEWS b/NEWS index c5177846..4661d927 100644 --- a/NEWS +++ b/NEWS @@ -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 diff --git a/borgmatic/borg/create.py b/borgmatic/borg/create.py index 4220bb21..54a09a83 100644 --- a/borgmatic/borg/create.py +++ b/borgmatic/borg/create.py @@ -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 diff --git a/tests/unit/borg/test_create.py b/tests/unit/borg/test_create.py index b8f4c4b8..ba693c77 100644 --- a/tests/unit/borg/test_create.py +++ b/tests/unit/borg/test_create.py @@ -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():