diff --git a/NEWS b/NEWS index 51b42e3..9a596ee 100644 --- a/NEWS +++ b/NEWS @@ -8,6 +8,7 @@ * #60: Add "Persistent" flag to systemd timer example. * #63: Support for Borg --nobsdflags option to skip recording bsdflags (e.g. NODUMP, IMMUTABLE) in archive. + * #61: Support for Borg --list option via borgmatic command-line to list all archives. 1.1.15 * Support for Borg BORG_PASSCOMMAND environment variable to read a password from an external file. diff --git a/borgmatic/borg/prune.py b/borgmatic/borg/prune.py index 3054d81..c6e0ce6 100644 --- a/borgmatic/borg/prune.py +++ b/borgmatic/borg/prune.py @@ -36,7 +36,7 @@ def prune_archives(verbosity, dry_run, repository, storage_config, retention_con local_path='borg', remote_path=None): ''' Given verbosity/dry-run flags, a local or remote repository path, a storage config dict, and a - retention config dict, prune Borg archives according the the retention policy specified in that + retention config dict, prune Borg archives according to the retention policy specified in that configuration. ''' remote_path_flags = ('--remote-path', remote_path) if remote_path else () diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index e2bffee..b9678b7 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -5,7 +5,8 @@ import os from subprocess import CalledProcessError import sys -from borgmatic.borg import check, create, prune +from borgmatic.borg import check as borg_check, create as borg_create, prune as borg_prune, \ + list as borg_list from borgmatic.commands import hook from borgmatic.config import collect, convert, validate from borgmatic.signals import configure_signals @@ -61,11 +62,17 @@ def parse_arguments(*arguments): action='store_true', help='Check archives for consistency', ) + parser.add_argument( + '-l', '--list', + dest='list', + action='store_true', + help='List archives', + ) 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', + help='Go through the motions, but do not actually write to any repositories', ) parser.add_argument( '-v', '--verbosity', @@ -75,10 +82,9 @@ def parse_arguments(*arguments): args = parser.parse_args(arguments) - # If any of the three action flags in the given parse arguments have been explicitly requested, - # leave them as-is. Otherwise, assume defaults: Mutate the given arguments to enable all the - # actions. - if not args.prune and not args.create and not args.check: + # If any of the action flags are explicitly requested, leave them as-is. Otherwise, assume + # defaults: Mutate the given arguments to enable the default actions. + if not args.prune and not args.create and not args.check and not args.list: args.prune = True args.create = True args.check = True @@ -101,7 +107,7 @@ def run_configuration(config_filename, args): # pragma: no cover try: local_path = location.get('local_path', 'borg') remote_path = location.get('remote_path') - create.initialize_environment(storage) + borg_create.initialize_environment(storage) hook.execute_hook(hooks.get('before_backup'), config_filename, 'pre-backup') for unexpanded_repository in location['repositories']: @@ -109,7 +115,7 @@ def run_configuration(config_filename, args): # pragma: no cover dry_run_label = ' (dry run; not making any changes)' if args.dry_run else '' if args.prune: logger.info('{}: Pruning archives{}'.format(repository, dry_run_label)) - prune.prune_archives( + borg_prune.prune_archives( args.verbosity, args.dry_run, repository, @@ -120,7 +126,7 @@ def run_configuration(config_filename, args): # pragma: no cover ) if args.create: logger.info('{}: Creating archive{}'.format(repository, dry_run_label)) - create.create_archive( + borg_create.create_archive( args.verbosity, args.dry_run, repository, @@ -131,14 +137,23 @@ def run_configuration(config_filename, args): # pragma: no cover ) if args.check: logger.info('{}: Running consistency checks'.format(repository)) - check.check_archives( - args.verbosity, + borg_check.check_archives( + args.verbosity, repository, storage, consistency, local_path=local_path, remote_path=remote_path, ) + if args.list: + logger.info('{}: Listing archives'.format(repository)) + borg_list.list_archives( + args.verbosity, + repository, + storage, + local_path=local_path, + remote_path=remote_path, + ) hook.execute_hook(hooks.get('after_backup'), config_filename, 'post-backup') except (OSError, CalledProcessError): diff --git a/borgmatic/tests/unit/borg/test_list.py b/borgmatic/tests/unit/borg/test_list.py new file mode 100644 index 0000000..1c6a69f --- /dev/null +++ b/borgmatic/tests/unit/borg/test_list.py @@ -0,0 +1,77 @@ +from collections import OrderedDict + +from flexmock import flexmock + +from borgmatic.borg import list as module +from borgmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS + + +def insert_subprocess_mock(check_call_command, **kwargs): + subprocess = flexmock(module.subprocess) + subprocess.should_receive('check_call').with_args(check_call_command, **kwargs).once() + + +LIST_COMMAND = ('borg', 'list', 'repo') + + +def test_list_archives_calls_borg_with_parameters(): + insert_subprocess_mock(LIST_COMMAND) + + module.list_archives( + verbosity=None, + repository='repo', + storage_config={}, + ) + + +def test_list_archives_with_verbosity_some_calls_borg_with_info_parameter(): + insert_subprocess_mock(LIST_COMMAND + ('--info',)) + + module.list_archives( + repository='repo', + storage_config={}, + verbosity=VERBOSITY_SOME, + ) + + +def test_list_archives_with_verbosity_lots_calls_borg_with_debug_parameter(): + insert_subprocess_mock(LIST_COMMAND + ('--debug',)) + + module.list_archives( + repository='repo', + storage_config={}, + verbosity=VERBOSITY_LOTS, + ) + + +def test_list_archives_with_local_path_calls_borg_via_local_path(): + insert_subprocess_mock(('borg1',) + LIST_COMMAND[1:]) + + module.list_archives( + verbosity=None, + repository='repo', + storage_config={}, + local_path='borg1', + ) + + +def test_list_archives_with_remote_path_calls_borg_with_remote_path_parameters(): + insert_subprocess_mock(LIST_COMMAND + ('--remote-path', 'borg1')) + + module.list_archives( + verbosity=None, + repository='repo', + storage_config={}, + remote_path='borg1', + ) + + +def test_list_archives_with_lock_wait_calls_borg_with_lock_wait_parameters(): + storage_config = {'lock_wait': 5} + insert_subprocess_mock(LIST_COMMAND + ('--lock-wait', '5')) + + module.list_archives( + verbosity=None, + repository='repo', + storage_config=storage_config, + )