diff --git a/NEWS b/NEWS index 1b445b754..8e880a016 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,6 @@ 1.1.14.dev0 - * #49: Rename incorrect --pattern-from option to correct --patterns-from. + * #49: Fix for typo in --patterns-from option. + * #47: Support for Borg --dry-run option via borgmatic command-line. 1.1.13 * #54: Fix for incorrect consistency check flags passed to Borg when all three checks ("repository", diff --git a/borgmatic/borg/create.py b/borgmatic/borg/create.py index d21e667cc..2fc538138 100644 --- a/borgmatic/borg/create.py +++ b/borgmatic/borg/create.py @@ -85,11 +85,11 @@ def _make_exclude_flags(location_config, exclude_filename=None): def create_archive( - verbosity, repository, location_config, storage_config, local_path='borg', remote_path=None, + verbosity, dry_run, repository, location_config, storage_config, local_path='borg', remote_path=None, ): ''' - Given a vebosity flag, a local or remote repository path, a location config dict, and a storage - config dict, create a Borg archive. + Given vebosity/dry-run flags, a local or remote repository path, a location config dict, and a + storage config dict, create a Borg archive. ''' sources = tuple( itertools.chain.from_iterable( @@ -122,6 +122,7 @@ def create_archive( VERBOSITY_SOME: ('--info', '--stats',), VERBOSITY_LOTS: ('--debug', '--list', '--stats'), }.get(verbosity, ()) + dry_run_flags = ('--dry-run',) if dry_run else () default_archive_name_format = '{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}' archive_name_format = storage_config.get('archive_name_format', default_archive_name_format) @@ -133,7 +134,7 @@ def create_archive( ), ) + sources + pattern_flags + exclude_flags + compression_flags + remote_rate_limit_flags + \ one_file_system_flags + files_cache_flags + remote_path_flags + umask_flags + \ - verbosity_flags + verbosity_flags + dry_run_flags logger.debug(' '.join(full_command)) subprocess.check_call(full_command) diff --git a/borgmatic/borg/prune.py b/borgmatic/borg/prune.py index 83890de94..dc66d3851 100644 --- a/borgmatic/borg/prune.py +++ b/borgmatic/borg/prune.py @@ -32,16 +32,17 @@ def _make_prune_flags(retention_config): ) -def prune_archives(verbosity, repository, retention_config, local_path='borg', remote_path=None): +def prune_archives(verbosity, dry_run, repository, retention_config, local_path='borg', remote_path=None): ''' - Given a verbosity flag, a local or remote repository path, a retention config dict, prune Borg - archives according the the retention policy specified in that configuration. + Given verbosity/dry-run flags, a local or remote repository path, a retention config dict, prune + Borg archives according the the retention policy specified in that configuration. ''' remote_path_flags = ('--remote-path', remote_path) if remote_path else () verbosity_flags = { VERBOSITY_SOME: ('--info', '--stats',), VERBOSITY_LOTS: ('--debug', '--stats', '--list'), }.get(verbosity, ()) + dry_run_flags = ('--dry-run',) if dry_run else () full_command = ( local_path, 'prune', @@ -50,7 +51,7 @@ def prune_archives(verbosity, repository, retention_config, local_path='borg', r element for pair in _make_prune_flags(retention_config) for element in pair - ) + remote_path_flags + verbosity_flags + ) + remote_path_flags + verbosity_flags + dry_run_flags logger.debug(' '.join(full_command)) subprocess.check_call(full_command) diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index d1772796c..da53040ac 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -61,6 +61,12 @@ def parse_arguments(*arguments): action='store_true', help='Check archives for consistency', ) + parser.add_argument( + '-n', '--dry-run', + dest='dry_run', + action='store_true', + help='Go through the motions, but do not actually write any changes to the repository', + ) parser.add_argument( '-v', '--verbosity', type=int, @@ -100,19 +106,22 @@ def run_configuration(config_filename, args): # pragma: no cover for unexpanded_repository in location['repositories']: repository = os.path.expanduser(unexpanded_repository) + dry_run_label = ' (dry run; not making any changes)' if args.dry_run else '' if args.prune: - logger.info('{}: Pruning archives'.format(repository)) + logger.info('{}: Pruning archives{}'.format(repository, dry_run_label)) prune.prune_archives( args.verbosity, + args.dry_run, repository, retention, local_path=local_path, remote_path=remote_path, ) if args.create: - logger.info('{}: Creating archive'.format(repository)) + logger.info('{}: Creating archive{}'.format(repository, dry_run_label)) create.create_archive( args.verbosity, + args.dry_run, repository, location, storage, diff --git a/borgmatic/tests/unit/borg/test_create.py b/borgmatic/tests/unit/borg/test_create.py index 5299f89b0..14b6c5821 100644 --- a/borgmatic/tests/unit/borg/test_create.py +++ b/borgmatic/tests/unit/borg/test_create.py @@ -190,6 +190,7 @@ def test_create_archive_calls_borg_with_parameters(): module.create_archive( verbosity=None, + dry_run=False, repository='repo', location_config={ 'source_directories': ['foo', 'bar'], @@ -210,6 +211,7 @@ def test_create_archive_with_patterns_calls_borg_with_patterns(): module.create_archive( verbosity=None, + dry_run=False, repository='repo', location_config={ 'source_directories': ['foo', 'bar'], @@ -230,6 +232,7 @@ def test_create_archive_with_exclude_patterns_calls_borg_with_excludes(): module.create_archive( verbosity=None, + dry_run=False, repository='repo', location_config={ 'source_directories': ['foo', 'bar'], @@ -250,6 +253,7 @@ def test_create_archive_with_verbosity_some_calls_borg_with_info_parameter(): module.create_archive( verbosity=VERBOSITY_SOME, + dry_run=False, repository='repo', location_config={ 'source_directories': ['foo', 'bar'], @@ -269,6 +273,28 @@ def test_create_archive_with_verbosity_lots_calls_borg_with_debug_parameter(): module.create_archive( verbosity=VERBOSITY_LOTS, + dry_run=False, + repository='repo', + location_config={ + 'source_directories': ['foo', 'bar'], + 'repositories': ['repo'], + 'exclude_patterns': None, + }, + storage_config={}, + ) + + +def test_create_archive_with_dry_run_calls_borg_with_dry_run_parameter(): + flexmock(module).should_receive('_expand_directory').and_return(['foo']).and_return(['bar']) + flexmock(module).should_receive('_write_pattern_file').and_return(None) + flexmock(module).should_receive('_make_pattern_flags').and_return(()) + flexmock(module).should_receive('_make_pattern_flags').and_return(()) + flexmock(module).should_receive('_make_exclude_flags').and_return(()) + insert_subprocess_mock(CREATE_COMMAND + ('--dry-run',)) + + module.create_archive( + verbosity=None, + dry_run=True, repository='repo', location_config={ 'source_directories': ['foo', 'bar'], @@ -288,6 +314,7 @@ def test_create_archive_with_compression_calls_borg_with_compression_parameters( module.create_archive( verbosity=None, + dry_run=False, repository='repo', location_config={ 'source_directories': ['foo', 'bar'], @@ -307,6 +334,7 @@ def test_create_archive_with_remote_rate_limit_calls_borg_with_remote_ratelimit_ module.create_archive( verbosity=None, + dry_run=False, repository='repo', location_config={ 'source_directories': ['foo', 'bar'], @@ -326,6 +354,7 @@ def test_create_archive_with_one_file_system_calls_borg_with_one_file_system_par module.create_archive( verbosity=None, + dry_run=False, repository='repo', location_config={ 'source_directories': ['foo', 'bar'], @@ -346,6 +375,7 @@ def test_create_archive_with_files_cache_calls_borg_with_files_cache_parameters( module.create_archive( verbosity=None, + dry_run=False, repository='repo', location_config={ 'source_directories': ['foo', 'bar'], @@ -366,6 +396,7 @@ def test_create_archive_with_local_path_calls_borg_via_local_path(): module.create_archive( verbosity=None, + dry_run=False, repository='repo', location_config={ 'source_directories': ['foo', 'bar'], @@ -386,6 +417,7 @@ def test_create_archive_with_remote_path_calls_borg_with_remote_path_parameters( module.create_archive( verbosity=None, + dry_run=False, repository='repo', location_config={ 'source_directories': ['foo', 'bar'], @@ -406,6 +438,7 @@ def test_create_archive_with_umask_calls_borg_with_umask_parameters(): module.create_archive( verbosity=None, + dry_run=False, repository='repo', location_config={ 'source_directories': ['foo', 'bar'], @@ -426,6 +459,7 @@ def test_create_archive_with_source_directories_glob_expands(): module.create_archive( verbosity=None, + dry_run=False, repository='repo', location_config={ 'source_directories': ['foo*'], @@ -446,6 +480,7 @@ def test_create_archive_with_non_matching_source_directories_glob_passes_through module.create_archive( verbosity=None, + dry_run=False, repository='repo', location_config={ 'source_directories': ['foo*'], @@ -465,6 +500,7 @@ def test_create_archive_with_glob_calls_borg_with_expanded_directories(): module.create_archive( verbosity=None, + dry_run=False, repository='repo', location_config={ 'source_directories': ['foo*'], @@ -484,6 +520,7 @@ def test_create_archive_with_archive_name_format_calls_borg_with_archive_name(): module.create_archive( verbosity=None, + dry_run=False, repository='repo', location_config={ 'source_directories': ['foo', 'bar'], @@ -505,6 +542,7 @@ def test_create_archive_with_archive_name_format_accepts_borg_placeholders(): module.create_archive( verbosity=None, + dry_run=False, repository='repo', location_config={ 'source_directories': ['foo', 'bar'], diff --git a/borgmatic/tests/unit/borg/test_prune.py b/borgmatic/tests/unit/borg/test_prune.py index 64c553b95..31fe937e1 100644 --- a/borgmatic/tests/unit/borg/test_prune.py +++ b/borgmatic/tests/unit/borg/test_prune.py @@ -64,6 +64,7 @@ def test_prune_archives_calls_borg_with_parameters(): module.prune_archives( verbosity=None, + dry_run=False, repository='repo', retention_config=retention_config, ) @@ -79,6 +80,7 @@ def test_prune_archives_with_verbosity_some_calls_borg_with_info_parameter(): module.prune_archives( repository='repo', verbosity=VERBOSITY_SOME, + dry_run=False, retention_config=retention_config, ) @@ -93,6 +95,22 @@ def test_prune_archives_with_verbosity_lots_calls_borg_with_debug_parameter(): module.prune_archives( repository='repo', verbosity=VERBOSITY_LOTS, + dry_run=False, + retention_config=retention_config, + ) + + +def test_prune_archives_with_dry_run_calls_borg_with_dry_run_parameter(): + retention_config = flexmock() + flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return( + BASE_PRUNE_FLAGS, + ) + insert_subprocess_mock(PRUNE_COMMAND + ('--dry-run',)) + + module.prune_archives( + repository='repo', + verbosity=None, + dry_run=True, retention_config=retention_config, ) @@ -106,6 +124,7 @@ def test_prune_archives_with_local_path_calls_borg_via_local_path(): module.prune_archives( verbosity=None, + dry_run=False, repository='repo', retention_config=retention_config, local_path='borg1', @@ -121,6 +140,7 @@ def test_prune_archives_with_remote_path_calls_borg_with_remote_path_parameters( module.prune_archives( verbosity=None, + dry_run=False, repository='repo', retention_config=retention_config, remote_path='borg1',