diff --git a/borgmatic/actions/recreate.py b/borgmatic/actions/recreate.py new file mode 100644 index 00000000..3cef7762 --- /dev/null +++ b/borgmatic/actions/recreate.py @@ -0,0 +1,53 @@ +import logging + +import borgmatic.borg.recreate +import borgmatic.config.validate +from borgmatic.actions.create import collect_patterns, process_patterns + +logger = logging.getLogger(__name__) + + +def run_recreate( + repository, + config, + local_borg_version, + recreate_arguments, + global_arguments, + local_path, + remote_path, +): + ''' + Run the "recreate" action for the given repository. + ''' + if recreate_arguments.repository is None or borgmatic.config.validate.repositories_match( + repository, recreate_arguments.repository + ): + if recreate_arguments.archive: + logger.answer(f'Recreating archive {recreate_arguments.archive}') + else: + logger.answer('Recreating repository') + + # Collect and process patterns. + processed_patterns = process_patterns( + collect_patterns(config), borgmatic.config.paths.get_working_directory(config) + ) + + borgmatic.borg.recreate.recreate_archive( + repository['path'], + borgmatic.borg.repo_list.resolve_archive_name( + repository['path'], + recreate_arguments.archive, + config, + local_borg_version, + global_arguments, + local_path, + remote_path, + ), + config, + local_borg_version, + recreate_arguments, + global_arguments, + local_path=local_path, + remote_path=remote_path, + patterns=processed_patterns, + ) diff --git a/borgmatic/borg/recreate.py b/borgmatic/borg/recreate.py new file mode 100644 index 00000000..bea11b58 --- /dev/null +++ b/borgmatic/borg/recreate.py @@ -0,0 +1,100 @@ +import logging +import shlex + +import borgmatic.borg.environment +import borgmatic.config.paths +import borgmatic.execute +from borgmatic.borg import flags +from borgmatic.borg.create import make_exclude_flags, make_list_filter_flags, write_patterns_file + +logger = logging.getLogger(__name__) + + +def recreate_archive( + repository, + archive, + config, + local_borg_version, + recreate_arguments, + global_arguments, + local_path, + remote_path=None, + patterns=None, +): + ''' + Given a local or remote repository path, an archive name, a configuration dict, + the local Borg version string, an argparse.Namespace of recreate arguments, + an argparse.Namespace of global arguments, optional local and remote Borg paths. + + Executes the recreate command with the given arguments. + ''' + + lock_wait = config.get('lock_wait', None) + exclude_flags = make_exclude_flags(config) + compression = config.get('compression', None) + chunker_params = config.get('chunker_params', None) + # Available recompress MODES: 'if-different' (default), 'always', 'never' + recompress = config.get('recompress', None) + + # Write patterns to a temporary file and use that file with --patterns-from. + patterns_file = write_patterns_file( + patterns, borgmatic.config.paths.get_working_directory(config) + ) + + recreate_command = ( + (local_path, 'recreate') + + (('--remote-path', remote_path) if remote_path else ()) + + (('--log-json',) if global_arguments.log_json else ()) + + (('--lock-wait', str(lock_wait)) if lock_wait is not None else ()) + + (('--info',) if logger.getEffectiveLevel() == logging.INFO else ()) + + (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ()) + + (('--patterns-from', patterns_file.name) if patterns_file else ()) + + ( + ( + '--list', + '--filter', + make_list_filter_flags(local_borg_version, global_arguments.dry_run), + ) + if recreate_arguments.list + else () + ) + # Flag --target works only for a single archive + + (('--target', recreate_arguments.target) if recreate_arguments.target and archive else ()) + + ( + ('--comment', shlex.quote(recreate_arguments.comment)) + if recreate_arguments.comment + else () + ) + + (('--timestamp', recreate_arguments.timestamp) if recreate_arguments.timestamp else ()) + + (('--compression', compression) if compression else ()) + + (('--chunker-params', chunker_params) if chunker_params else ()) + + ( + flags.make_match_archives_flags( + recreate_arguments.match_archives or archive or config.get('match_archives'), + config.get('archive_name_format'), + local_borg_version, + ) + if recreate_arguments.match_archives + else () + ) + + (('--recompress', recompress) if recompress else ()) + + exclude_flags + + ( + flags.make_repository_archive_flags(repository, archive, local_borg_version) + if archive + else flags.make_repository_flags(repository, local_borg_version) + ) + ) + + if global_arguments.dry_run: + logger.info('Skipping the archive recreation (dry run)') + return + + borgmatic.execute.execute_command( + full_command=recreate_command, + output_log_level=logging.INFO, + environment=borgmatic.borg.environment.make_environment(config), + working_directory=borgmatic.config.paths.get_working_directory(config), + borg_local_path=local_path, + borg_exit_codes=config.get('borg_exit_codes'), + ) diff --git a/borgmatic/commands/arguments.py b/borgmatic/commands/arguments.py index 56842ea8..6269a4e0 100644 --- a/borgmatic/commands/arguments.py +++ b/borgmatic/commands/arguments.py @@ -27,6 +27,7 @@ ACTION_ALIASES = { 'break-lock': [], 'key': [], 'borg': [], + 'recreate': [], } @@ -1545,6 +1546,52 @@ def make_parsers(): ) borg_group.add_argument('-h', '--help', action='help', help='Show this help message and exit') + recreate_parser = action_parsers.add_parser( + 'recreate', + aliases=ACTION_ALIASES['recreate'], + help='Recreate an archive in a repository', + description='Recreate an archive in a repository', + add_help=False, + ) + recreate_group = recreate_parser.add_argument_group('recreate arguments') + recreate_group.add_argument( + '--repository', + help='Path of repository containing archive to recreate, defaults to the configured repository if there is only one, quoted globs supported', + ) + recreate_group.add_argument( + '--archive', + help='Archive name, hash, or series to recreate', + ) + recreate_group.add_argument( + '--list', dest='list', action='store_true', help='Show per-file details' + ) + recreate_group.add_argument( + '--target', + metavar='TARGET', + help='Create a new archive from the specified archive (via --archive), without replacing it', + ) + recreate_group.add_argument( + '--comment', + metavar='COMMENT', + help='Add a comment text to the archive or, if an archive is not provided, to all matching archives', + ) + recreate_group.add_argument( + '--timestamp', + metavar='TIMESTAMP', + help='Manually override the archive creation date/time (UTC)', + ) + recreate_group.add_argument( + '-a', + '--match-archives', + '--glob-archives', + dest='match_archives', + metavar='PATTERN', + help='Only consider archive names, hashes, or series matching this pattern', + ) + recreate_group.add_argument( + '-h', '--help', action='help', help='Show this help message and exit' + ) + return global_parser, action_parsers, global_plus_action_parser diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index bd1ec1e6..53d9582c 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -26,6 +26,7 @@ import borgmatic.actions.info import borgmatic.actions.list import borgmatic.actions.mount import borgmatic.actions.prune +import borgmatic.actions.recreate import borgmatic.actions.repo_create import borgmatic.actions.repo_delete import borgmatic.actions.repo_info @@ -397,6 +398,16 @@ def run_actions( local_path, remote_path, ) + elif action_name == 'recreate' and action_name not in skip_actions: + borgmatic.actions.recreate.run_recreate( + repository, + config, + local_borg_version, + action_arguments, + global_arguments, + local_path, + remote_path, + ) elif action_name == 'prune' and action_name not in skip_actions: borgmatic.actions.prune.run_prune( config_filename, diff --git a/borgmatic/config/schema.yaml b/borgmatic/config/schema.yaml index bb0dc794..3154ddad 100644 --- a/borgmatic/config/schema.yaml +++ b/borgmatic/config/schema.yaml @@ -284,6 +284,23 @@ properties: http://borgbackup.readthedocs.io/en/stable/usage/create.html for details. Defaults to "lz4". example: lz4 + recompress: + type: string + enum: ['if-different', 'always', 'never'] + description: | + Mode for recompressing data chunks according to MODE. + Possible modes are: + - "if-different": Recompress if the current compression + is with a different compression algorithm. + - "always": Recompress even if the current compression + is with the same compression algorithm. Use this to change + the compression level. + - "never": Do not recompress. Use this option to explicitly + prevent recompression. + See https://borgbackup.readthedocs.io/en/stable/usage/recreate.html + for details. + Defaults to "never". + example: if-different upload_rate_limit: type: integer description: | @@ -767,6 +784,7 @@ properties: - prune - compact - create + - recreate - check - delete - extract @@ -982,6 +1000,7 @@ properties: - prune - compact - create + - recreate - check - delete - extract @@ -1046,6 +1065,7 @@ properties: - prune - compact - create + - recreate - check - delete - extract diff --git a/tests/unit/actions/test_recreate.py b/tests/unit/actions/test_recreate.py new file mode 100644 index 00000000..4250af2c --- /dev/null +++ b/tests/unit/actions/test_recreate.py @@ -0,0 +1,39 @@ +from flexmock import flexmock + +from borgmatic.actions import recreate as module + + +def test_run_recreate_does_not_raise(): + flexmock(module.logger).answer = lambda message: None + flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True) + flexmock(module.borgmatic.borg.recreate).should_receive('recreate_archive') + + recreate_arguments = flexmock(repository=flexmock(), archive=None) + + module.run_recreate( + repository={'path': 'repo'}, + config={}, + local_borg_version=None, + recreate_arguments=recreate_arguments, + global_arguments=flexmock(), + local_path=None, + remote_path=None, + ) + + +def test_run_recreate_with_archive_does_not_raise(): + flexmock(module.logger).answer = lambda message: None + flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True) + flexmock(module.borgmatic.borg.recreate).should_receive('recreate_archive') + + recreate_arguments = flexmock(repository=flexmock(), archive='test-archive') + + module.run_recreate( + repository={'path': 'repo'}, + config={}, + local_borg_version=None, + recreate_arguments=recreate_arguments, + global_arguments=flexmock(), + local_path=None, + remote_path=None, + ) diff --git a/tests/unit/borg/test_recreate.py b/tests/unit/borg/test_recreate.py new file mode 100644 index 00000000..fe2739b7 --- /dev/null +++ b/tests/unit/borg/test_recreate.py @@ -0,0 +1,644 @@ +import logging +import shlex + +from flexmock import flexmock + +from borgmatic.borg import recreate as module + +from ..test_verbosity import insert_logging_mock + + +def insert_execute_command_mock(command, working_directory=None, borg_exit_codes=None): + flexmock(module.borgmatic.borg.environment).should_receive('make_environment') + flexmock(module.borgmatic.execute).should_receive('execute_command').with_args( + full_command=command, + output_log_level=module.logging.INFO, + environment=None, + working_directory=working_directory, + borg_local_path=command[0], + borg_exit_codes=borg_exit_codes, + ).once() + + +def mock_dependencies(): + flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive( + 'make_repository_archive_flags' + ).and_return(('repo::archive',)) + + +def test_recreate_archive_dry_run_skips_execution(): + flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive( + 'make_repository_archive_flags' + ).and_return(('repo::archive',)) + flexmock(module.borgmatic.execute).should_receive('execute_command').never() + + recreate_arguments = flexmock( + repository=flexmock(), + list=None, + target=None, + comment=None, + timestamp=None, + match_archives=None, + ) + + result = module.recreate_archive( + repository='repo', + archive='archive', + config={}, + local_borg_version='1.2.3', + recreate_arguments=recreate_arguments, + global_arguments=flexmock(log_json=False, dry_run=True), + local_path='borg', + ) + + assert result is None + + +def test_recreate_calls_borg_with_required_flags(): + flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive( + 'make_repository_archive_flags' + ).and_return(('repo::archive',)) + insert_execute_command_mock(('borg', 'recreate', 'repo::archive')) + + module.recreate_archive( + repository='repo', + archive='archive', + config={}, + local_borg_version='1.2.3', + recreate_arguments=flexmock( + list=None, + target=None, + comment=None, + timestamp=None, + match_archives=None, + ), + global_arguments=flexmock(dry_run=False, log_json=False), + local_path='borg', + remote_path=None, + patterns=None, + ) + + +def test_recreate_with_remote_path(): + flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive( + 'make_repository_archive_flags' + ).and_return(('repo::archive',)) + insert_execute_command_mock(('borg', 'recreate', '--remote-path', 'borg1', 'repo::archive')) + + module.recreate_archive( + repository='repo', + archive='archive', + config={}, + local_borg_version='1.2.3', + recreate_arguments=flexmock( + list=None, + target=None, + comment=None, + timestamp=None, + match_archives=None, + ), + global_arguments=flexmock(dry_run=False, log_json=False), + local_path='borg', + remote_path='borg1', + patterns=None, + ) + + +def test_recreate_with_lock_wait(): + flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive( + 'make_repository_archive_flags' + ).and_return(('repo::archive',)) + insert_execute_command_mock(('borg', 'recreate', '--lock-wait', '5', 'repo::archive')) + + module.recreate_archive( + repository='repo', + archive='archive', + config={'lock_wait': '5'}, + local_borg_version='1.2.3', + recreate_arguments=flexmock( + list=None, + target=None, + comment=None, + timestamp=None, + match_archives=None, + ), + global_arguments=flexmock(dry_run=False, log_json=False), + local_path='borg', + patterns=None, + ) + + +def test_recreate_with_log_info(): + flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive( + 'make_repository_archive_flags' + ).and_return(('repo::archive',)) + insert_execute_command_mock(('borg', 'recreate', '--info', 'repo::archive')) + + insert_logging_mock(logging.INFO) + + module.recreate_archive( + repository='repo', + archive='archive', + config={}, + local_borg_version='1.2.3', + recreate_arguments=flexmock( + list=None, + target=None, + comment=None, + timestamp=None, + match_archives=None, + ), + global_arguments=flexmock(dry_run=False, log_json=False), + local_path='borg', + patterns=None, + ) + + +def test_recreate_with_log_debug(): + flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive( + 'make_repository_archive_flags' + ).and_return(('repo::archive',)) + insert_execute_command_mock(('borg', 'recreate', '--debug', '--show-rc', 'repo::archive')) + insert_logging_mock(logging.DEBUG) + + module.recreate_archive( + repository='repo', + archive='archive', + config={}, + local_borg_version='1.2.3', + recreate_arguments=flexmock( + list=None, + target=None, + comment=None, + timestamp=None, + match_archives=None, + ), + global_arguments=flexmock(dry_run=False, log_json=False), + local_path='borg', + patterns=None, + ) + + +def test_recreate_with_log_json(): + flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive( + 'make_repository_archive_flags' + ).and_return(('repo::archive',)) + insert_execute_command_mock(('borg', 'recreate', '--log-json', 'repo::archive')) + + module.recreate_archive( + repository='repo', + archive='archive', + config={}, + local_borg_version='1.2.3', + recreate_arguments=flexmock( + list=None, + target=None, + comment=None, + timestamp=None, + match_archives=None, + ), + global_arguments=flexmock(dry_run=False, log_json=True), + local_path='borg', + patterns=None, + ) + + +def test_recreate_with_list_filter_flags(): + flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive( + 'make_repository_archive_flags' + ).and_return(('repo::archive',)) + flexmock(module).should_receive('make_list_filter_flags').and_return('AME+-') + insert_execute_command_mock( + ('borg', 'recreate', '--list', '--filter', 'AME+-', 'repo::archive') + ) + + module.recreate_archive( + repository='repo', + archive='archive', + config={}, + local_borg_version='1.2.3', + recreate_arguments=flexmock( + list=True, + target=None, + comment=None, + timestamp=None, + match_archives=None, + ), + global_arguments=flexmock(dry_run=False, log_json=False), + local_path='borg', + patterns=None, + ) + + +def test_recreate_with_patterns_from_flag(): + flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive( + 'make_repository_archive_flags' + ).and_return(('repo::archive',)) + mock_patterns_file = flexmock(name='patterns_file') + flexmock(module).should_receive('write_patterns_file').and_return(mock_patterns_file) + insert_execute_command_mock( + ('borg', 'recreate', '--patterns-from', 'patterns_file', 'repo::archive') + ) + + module.recreate_archive( + repository='repo', + archive='archive', + config={}, + local_borg_version='1.2.3', + recreate_arguments=flexmock( + list=None, + target=None, + comment=None, + timestamp=None, + match_archives=None, + ), + global_arguments=flexmock(dry_run=False, log_json=False), + local_path='borg', + patterns=['pattern1', 'pattern2'], + ) + + +def test_recreate_with_exclude_flags(): + flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive( + 'make_repository_archive_flags' + ).and_return(('repo::archive',)) + flexmock(module).should_receive('make_exclude_flags').and_return(('--exclude', 'pattern')) + insert_execute_command_mock(('borg', 'recreate', '--exclude', 'pattern', 'repo::archive')) + + module.recreate_archive( + repository='repo', + archive='archive', + config={'exclude_patterns': ['pattern']}, + local_borg_version='1.2.3', + recreate_arguments=flexmock( + list=None, + target=None, + comment=None, + timestamp=None, + match_archives=None, + ), + global_arguments=flexmock(dry_run=False, log_json=False), + local_path='borg', + patterns=None, + ) + + +def test_recreate_with_target_flag(): + flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive( + 'make_repository_archive_flags' + ).and_return(('repo::archive',)) + insert_execute_command_mock(('borg', 'recreate', '--target', 'new-archive', 'repo::archive')) + + module.recreate_archive( + repository='repo', + archive='archive', + config={}, + local_borg_version='1.2.3', + recreate_arguments=flexmock( + list=None, + target='new-archive', + comment=None, + timestamp=None, + match_archives=None, + ), + global_arguments=flexmock(dry_run=False, log_json=False), + local_path='borg', + patterns=None, + ) + + +def test_recreate_with_comment_flag(): + flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive( + 'make_repository_archive_flags' + ).and_return(('repo::archive',)) + insert_execute_command_mock( + ('borg', 'recreate', '--comment', shlex.quote('This is a test comment'), 'repo::archive') + ) + + module.recreate_archive( + repository='repo', + archive='archive', + config={}, + local_borg_version='1.2.3', + recreate_arguments=flexmock( + list=None, + target=None, + comment='This is a test comment', + timestamp=None, + match_archives=None, + ), + global_arguments=flexmock(dry_run=False, log_json=False), + local_path='borg', + patterns=None, + ) + + +def test_recreate_with_timestamp_flag(): + flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive( + 'make_repository_archive_flags' + ).and_return(('repo::archive',)) + insert_execute_command_mock( + ('borg', 'recreate', '--timestamp', '2023-10-01T12:00:00', 'repo::archive') + ) + + module.recreate_archive( + repository='repo', + archive='archive', + config={}, + local_borg_version='1.2.3', + recreate_arguments=flexmock( + list=None, + target=None, + comment=None, + timestamp='2023-10-01T12:00:00', + match_archives=None, + ), + global_arguments=flexmock(dry_run=False, log_json=False), + local_path='borg', + patterns=None, + ) + + +def test_recreate_with_compression_flag(): + flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive( + 'make_repository_archive_flags' + ).and_return(('repo::archive',)) + insert_execute_command_mock(('borg', 'recreate', '--compression', 'lz4', 'repo::archive')) + + module.recreate_archive( + repository='repo', + archive='archive', + config={'compression': 'lz4'}, + local_borg_version='1.2.3', + recreate_arguments=flexmock( + list=None, + target=None, + comment=None, + timestamp=None, + match_archives=None, + ), + global_arguments=flexmock(dry_run=False, log_json=False), + local_path='borg', + patterns=None, + ) + + +def test_recreate_with_chunker_params_flag(): + flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive( + 'make_repository_archive_flags' + ).and_return(('repo::archive',)) + insert_execute_command_mock( + ('borg', 'recreate', '--chunker-params', '19,23,21,4095', 'repo::archive') + ) + + module.recreate_archive( + repository='repo', + archive='archive', + config={'chunker_params': '19,23,21,4095'}, + local_borg_version='1.2.3', + recreate_arguments=flexmock( + list=None, + target=None, + comment=None, + timestamp=None, + match_archives=None, + ), + global_arguments=flexmock(dry_run=False, log_json=False), + local_path='borg', + patterns=None, + ) + + +def test_recreate_with_recompress_flag(): + flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive( + 'make_repository_archive_flags' + ).and_return(('repo::archive',)) + insert_execute_command_mock(('borg', 'recreate', '--recompress', 'always', 'repo::archive')) + + module.recreate_archive( + repository='repo', + archive='archive', + config={'recompress': 'always'}, + local_borg_version='1.2.3', + recreate_arguments=flexmock( + list=None, + target=None, + comment=None, + timestamp=None, + match_archives=None, + ), + global_arguments=flexmock(dry_run=False, log_json=False), + local_path='borg', + patterns=None, + ) + + +def test_recreate_with_match_archives_star(): + flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive( + 'make_repository_archive_flags' + ).and_return(('repo::archive',)) + insert_execute_command_mock(('borg', 'recreate', 'repo::archive')) + + module.recreate_archive( + repository='repo', + archive='archive', + config={}, + local_borg_version='1.2.3', + recreate_arguments=flexmock( + list=None, + target=None, + comment=None, + timestamp=None, + match_archives='*', + ), + global_arguments=flexmock(dry_run=False, log_json=False), + local_path='borg', + patterns=None, + ) + + +def test_recreate_with_match_archives_regex(): + flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive( + 'make_repository_archive_flags' + ).and_return(('repo::archive',)) + insert_execute_command_mock(('borg', 'recreate', 'repo::archive')) + + module.recreate_archive( + repository='repo', + archive='archive', + config={}, + local_borg_version='1.2.3', + recreate_arguments=flexmock( + list=None, + target=None, + comment=None, + timestamp=None, + match_archives='re:.*', + ), + global_arguments=flexmock(dry_run=False, log_json=False), + local_path='borg', + patterns=None, + ) + + +def test_recreate_with_match_archives_shell(): + flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(()) + flexmock(module.borgmatic.borg.flags).should_receive( + 'make_repository_archive_flags' + ).and_return(('repo::archive',)) + insert_execute_command_mock(('borg', 'recreate', 'repo::archive')) + + module.recreate_archive( + repository='repo', + archive='archive', + config={}, + local_borg_version='1.2.3', + recreate_arguments=flexmock( + list=None, + target=None, + comment=None, + timestamp=None, + match_archives='sh:*', + ), + global_arguments=flexmock(dry_run=False, log_json=False), + local_path='borg', + patterns=None, + ) + + +def test_recreate_with_glob_archives_flag(): + flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return( + ('--glob-archives', 'foo-*') + ) + flexmock(module.borgmatic.borg.flags).should_receive( + 'make_repository_archive_flags' + ).and_return(('repo::archive',)) + insert_execute_command_mock(('borg', 'recreate', '--glob-archives', 'foo-*', 'repo::archive')) + + module.recreate_archive( + repository='repo', + archive='archive', + config={}, + local_borg_version='1.2.3', + recreate_arguments=flexmock( + list=None, + target=None, + comment=None, + timestamp=None, + match_archives='foo-*', + ), + global_arguments=flexmock(dry_run=False, log_json=False), + local_path='borg', + patterns=None, + ) + + +def test_recreate_with_match_archives_flag(): + flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(()) + flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('') + flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return( + ('--match-archives', 'sh:foo-*') + ) + flexmock(module.borgmatic.borg.flags).should_receive( + 'make_repository_archive_flags' + ).and_return(('--repo', 'repo', 'archive')) + insert_execute_command_mock( + ('borg', 'recreate', '--match-archives', 'sh:foo-*', '--repo', 'repo', 'archive') + ) + + module.recreate_archive( + repository='repo', + archive='archive', + config={}, + local_borg_version='2.0.0b3', + recreate_arguments=flexmock( + list=None, + target=None, + comment=None, + timestamp=None, + match_archives='sh:foo-*', + ), + global_arguments=flexmock(dry_run=False, log_json=False), + local_path='borg', + patterns=None, + ) diff --git a/tests/unit/commands/test_borgmatic.py b/tests/unit/commands/test_borgmatic.py index 160f4454..481a856a 100644 --- a/tests/unit/commands/test_borgmatic.py +++ b/tests/unit/commands/test_borgmatic.py @@ -1039,6 +1039,47 @@ def test_run_actions_with_skip_actions_skips_create(): ) +def test_run_actions_runs_recreate(): + flexmock(module).should_receive('add_custom_log_levels') + flexmock(module).should_receive('get_skip_actions').and_return([]) + flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock()) + + flexmock(borgmatic.actions.recreate).should_receive('run_recreate').once() + + tuple( + module.run_actions( + arguments={'global': flexmock(dry_run=False, log_file='foo'), 'recreate': flexmock()}, + config_filename=flexmock(), + config={'repositories': []}, + config_paths=[], + local_path=flexmock(), + remote_path=flexmock(), + local_borg_version=flexmock(), + repository={'path': 'repo'}, + ) + ) + + +def test_run_actions_with_skip_actions_skips_recreate(): + flexmock(module).should_receive('add_custom_log_levels') + flexmock(module).should_receive('get_skip_actions').and_return(['recreate']) + flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock()) + flexmock(borgmatic.actions.recreate).should_receive('run_recreate').never() + + tuple( + module.run_actions( + arguments={'global': flexmock(dry_run=False, log_file='foo'), 'recreate': flexmock()}, + config_filename=flexmock(), + config={'repositories': [], 'skip_actions': ['recreate']}, + config_paths=[], + local_path=flexmock(), + remote_path=flexmock(), + local_borg_version=flexmock(), + repository={'path': 'repo'}, + ) + ) + + def test_run_actions_runs_prune(): flexmock(module).should_receive('add_custom_log_levels') flexmock(module).should_receive('get_skip_actions').and_return([])