diff --git a/borgmatic/borg/create.py b/borgmatic/borg/create.py index e55f8dcb..2d7d58d5 100644 --- a/borgmatic/borg/create.py +++ b/borgmatic/borg/create.py @@ -49,6 +49,7 @@ def validate_planned_backup_paths( dry_run, create_command, config, + patterns, local_path, working_directory, borgmatic_runtime_directory, @@ -91,21 +92,32 @@ def validate_planned_backup_paths( if path_line and path_line.startswith(('- ', '+ ')) ) - # These are the subset of those files that contain the borgmatic runtime directory. - paths_containing_runtime_directory = {} + # These are the subset of output paths contained within the borgmatic runtime directory. + paths_inside_runtime_directory = { + path for path in paths if any_parent_directories(path, (borgmatic_runtime_directory,)) + } - if os.path.exists(borgmatic_runtime_directory): - paths_containing_runtime_directory = { - path for path in paths if any_parent_directories(path, (borgmatic_runtime_directory,)) - } + # If the runtime directory isn't present in the source patterns, then we shouldn't expect it to + # be in the paths output from the Borg dry run. + runtime_directory_present_in_patterns = any( + pattern + for pattern in patterns + if any_parent_directories(pattern.path, (borgmatic_runtime_directory,)) + if pattern.type == borgmatic.borg.pattern.Pattern_type.ROOT + ) - # If no paths to backup contain the runtime directory, it must've been excluded. - if not paths_containing_runtime_directory and not dry_run: - 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 no paths to backup are inside the runtime directory, it must've been excluded. + if ( + not paths_inside_runtime_directory + and runtime_directory_present_in_patterns + and not dry_run + and os.path.exists(borgmatic_runtime_directory) + ): + 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.', + ) - return tuple(path for path in paths if path not in paths_containing_runtime_directory) + return tuple(path for path in paths if path not in paths_inside_runtime_directory) MAX_SPECIAL_FILE_PATHS_LENGTH = 1000 @@ -227,6 +239,7 @@ def make_base_create_command( dry_run, create_flags + create_positional_arguments, config, + patterns, local_path, working_directory, borgmatic_runtime_directory=borgmatic_runtime_directory, diff --git a/tests/unit/borg/test_create.py b/tests/unit/borg/test_create.py index a956b458..b200a1c8 100644 --- a/tests/unit/borg/test_create.py +++ b/tests/unit/borg/test_create.py @@ -71,12 +71,17 @@ def test_validate_planned_backup_paths_parses_borg_dry_run_file_list(): 'Processing files ...\n- /foo\n+ /bar\n- /baz', ) flexmock(module.os.path).should_receive('exists').and_return(False) - flexmock(module).should_receive('any_parent_directories').never() + flexmock(module).should_receive('any_parent_directories').and_return(False) 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('/bar'), + module.borgmatic.borg.pattern.Pattern('/baz'), + ), local_path=None, working_directory=None, borgmatic_runtime_directory='/run/borgmatic', @@ -95,23 +100,21 @@ def test_validate_planned_backup_paths_skips_borgmatic_runtime_directory(): '+ /foo\n- /run/borgmatic/bar\n- /baz', ) flexmock(module.os.path).should_receive('exists').and_return(True) - flexmock(module).should_receive('any_parent_directories').with_args( - '/foo', - ('/run/borgmatic',), - ).and_return(False) - flexmock(module).should_receive('any_parent_directories').with_args( - '/run/borgmatic/bar', - ('/run/borgmatic',), - ).and_return(True) - flexmock(module).should_receive('any_parent_directories').with_args( - '/baz', - ('/run/borgmatic',), - ).and_return(False) + flexmock(module).should_receive('any_parent_directories').replace_with( + lambda path, _: path == '/run/borgmatic/bar' + ) 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('/baz'), + ), local_path=None, working_directory=None, borgmatic_runtime_directory='/run/borgmatic', @@ -130,19 +133,58 @@ def test_validate_planned_backup_paths_with_borgmatic_runtime_directory_missing_ '+ /foo\n- /bar\n- /baz', ) flexmock(module.os.path).should_receive('exists').and_return(True) - flexmock(module).should_receive('any_parent_directories').and_return(False) + flexmock(module).should_receive('any_parent_directories').replace_with( + lambda path, _: path == '/run/borgmatic/bar' + ) 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'), + ), local_path=None, working_directory=None, borgmatic_runtime_directory='/run/borgmatic', ) +def test_validate_planned_backup_paths_with_borgmatic_runtime_directory_missing_from_patterns_does_not_raise(): + 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- /run/borgmatic/bar\n- /baz', + ) + flexmock(module.os.path).should_receive('exists').and_return(True) + flexmock(module).should_receive('any_parent_directories').replace_with( + lambda path, _: path == '/run/borgmatic/bar' + ) + + 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('/baz'), + ), + local_path=None, + working_directory=None, + borgmatic_runtime_directory='/run/borgmatic', + ) == ('/foo', '/baz') + + def test_validate_planned_backup_paths_with_dry_run_and_borgmatic_runtime_directory_missing_from_paths_output_does_not_raise(): flexmock(module.flags).should_receive('omit_flag').replace_with( lambda arguments, flag: arguments, @@ -152,7 +194,7 @@ def test_validate_planned_backup_paths_with_dry_run_and_borgmatic_runtime_direct ) 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', + '+ /foo\n- /run/borgmatic/bar\n- /baz', ) flexmock(module.os.path).should_receive('exists').and_return(True) flexmock(module).should_receive('any_parent_directories').and_return(False) @@ -161,10 +203,17 @@ def test_validate_planned_backup_paths_with_dry_run_and_borgmatic_runtime_direct dry_run=True, 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'), + ), local_path=None, working_directory=None, borgmatic_runtime_directory='/run/borgmatic', - ) == ('/foo', '/bar', '/baz') + ) == ('/foo', '/run/borgmatic/bar', '/baz') DEFAULT_ARCHIVE_NAME = '{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}'