diff --git a/NEWS b/NEWS index 3140e045..5b4044fc 100644 --- a/NEWS +++ b/NEWS @@ -1,6 +1,8 @@ 1.3.11.dev0 - * #193: Pass through several "borg list" flags like --short, --format, --sort-by, --first, --last, - etc. via borgmatic list command-line flags. + * #193: Pass through several "borg list" and "borg info" flags like --short, --format, --sort-by, + --first, --last, etc. via borgmatic command-line flags. + * Add borgmatic info --repository and --archive command-line flags to display info for individual + repositories or archives. 1.3.10 * #198: Fix for Borg create error output not showing up at borgmatic verbosity level zero. diff --git a/borgmatic/borg/info.py b/borgmatic/borg/info.py index 6460a7bf..5b9369ff 100644 --- a/borgmatic/borg/info.py +++ b/borgmatic/borg/info.py @@ -1,26 +1,44 @@ import logging +from borgmatic.borg.flags import make_flags, make_flags_from_arguments from borgmatic.execute import execute_command logger = logging.getLogger(__name__) def display_archives_info( - repository, storage_config, local_path='borg', remote_path=None, json=False + repository, storage_config, info_arguments, local_path='borg', remote_path=None ): ''' - Given a local or remote repository path, and a storage config dict, display summary information - for Borg archives in the repository or return JSON summary information. + Given a local or remote repository path, a storage config dict, and the arguments to the info + action, display summary information for Borg archives in the repository or return JSON summary + information. ''' lock_wait = storage_config.get('lock_wait', None) full_command = ( - (local_path, 'info', repository) - + (('--remote-path', remote_path) if remote_path else ()) - + (('--lock-wait', str(lock_wait)) if lock_wait else ()) - + (('--info',) if logger.getEffectiveLevel() == logging.INFO and not json else ()) - + (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) and not json else ()) - + (('--json',) if json else ()) + ( + local_path, + 'info', + '::'.join((repository, info_arguments.archive)) + if info_arguments.archive + else repository, + ) + + ( + ('--info',) + if logger.getEffectiveLevel() == logging.INFO and not info_arguments.json + else () + ) + + ( + ('--debug', '--show-rc') + if logger.isEnabledFor(logging.DEBUG) and not info_arguments.json + else () + ) + + make_flags('remote-path', remote_path) + + make_flags('lock-wait', lock_wait) + + make_flags_from_arguments(info_arguments, excludes=('repository', 'archive')) ) - return execute_command(full_command, output_log_level=None if json else logging.WARNING) + return execute_command( + full_command, output_log_level=None if info_arguments.json else logging.WARNING + ) diff --git a/borgmatic/commands/arguments.py b/borgmatic/commands/arguments.py index 27c1e26e..3f11cf02 100644 --- a/borgmatic/commands/arguments.py +++ b/borgmatic/commands/arguments.py @@ -224,7 +224,7 @@ def parse_arguments(*unparsed_arguments): extract_group = extract_parser.add_argument_group('extract arguments') extract_group.add_argument( '--repository', - help='Path of repository to use, defaults to the configured repository if there is only one', + help='Path of repository to extract, defaults to the configured repository if there is only one', ) extract_group.add_argument('--archive', help='Name of archive to operate on', required=True) extract_group.add_argument( @@ -254,9 +254,9 @@ def parse_arguments(*unparsed_arguments): list_group = list_parser.add_argument_group('list arguments') list_group.add_argument( '--repository', - help='Path of repository to use, defaults to the configured repository if there is only one', + help='Path of repository to list, defaults to the configured repository if there is only one', ) - list_group.add_argument('--archive', help='Name of archive to operate on') + list_group.add_argument('--archive', help='Name of archive to list') list_group.add_argument( '--short', default=False, action='store_true', help='Output only archive or path names' ) @@ -301,9 +301,34 @@ def parse_arguments(*unparsed_arguments): add_help=False, ) info_group = info_parser.add_argument_group('info arguments') + info_group.add_argument( + '--repository', + help='Path of repository to show info for, defaults to the configured repository if there is only one', + ) + info_group.add_argument('--archive', help='Name of archive to show info for') info_group.add_argument( '--json', dest='json', default=False, action='store_true', help='Output results as JSON' ) + info_group.add_argument( + '-P', '--prefix', help='Only show info for archive names starting with this prefix' + ) + info_group.add_argument( + '-a', + '--glob-archives', + metavar='GLOB', + help='Only show info for archive names matching this glob', + ) + info_group.add_argument( + '--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys' + ) + info_group.add_argument( + '--first', + metavar='N', + help='Show info for first N archives after other filters are applied', + ) + info_group.add_argument( + '--last', metavar='N', help='Show info for first N archives after other filters are applied' + ) info_group.add_argument('-h', '--help', action='help', help='Show this help message and exit') arguments = parse_subparser_arguments(unparsed_arguments, top_level_parser, subparsers) diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index b2829202..63ddc8b4 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -178,16 +178,17 @@ def run_actions( if json_output: yield json.loads(json_output) if 'info' in arguments: - logger.info('{}: Displaying summary info for archives'.format(repository)) - json_output = borg_info.display_archives_info( - repository, - storage, - local_path=local_path, - remote_path=remote_path, - json=arguments['info'].json, - ) - if json_output: - yield json.loads(json_output) + if arguments['info'].repository is None or repository == arguments['info'].repository: + logger.info('{}: Displaying summary info for archives'.format(repository)) + json_output = borg_info.display_archives_info( + repository, + storage, + info_arguments=arguments['info'], + local_path=local_path, + remote_path=remote_path, + ) + if json_output: + yield json.loads(json_output) def load_configurations(config_filenames): diff --git a/tests/unit/borg/test_flags.py b/tests/unit/borg/test_flags.py index c11b270f..ca2ff63c 100644 --- a/tests/unit/borg/test_flags.py +++ b/tests/unit/borg/test_flags.py @@ -30,7 +30,9 @@ def test_make_flags_from_arguments_flattens_multiple_arguments(): ) arguments = flexmock(foo='bar', baz='quux') - assert module.make_flags_from_arguments(arguments) == ('foo', 'bar', 'baz', 'quux') + assert sorted(module.make_flags_from_arguments(arguments)) == sorted( + ('foo', 'bar', 'baz', 'quux') + ) def test_make_flags_from_arguments_excludes_underscored_argument_names(): diff --git a/tests/unit/borg/test_info.py b/tests/unit/borg/test_info.py index 9bf0dae4..4e301a16 100644 --- a/tests/unit/borg/test_info.py +++ b/tests/unit/borg/test_info.py @@ -1,5 +1,6 @@ import logging +import pytest from flexmock import flexmock from borgmatic.borg import info as module @@ -14,7 +15,9 @@ def test_display_archives_info_calls_borg_with_parameters(): INFO_COMMAND, output_log_level=logging.WARNING ) - module.display_archives_info(repository='repo', storage_config={}) + module.display_archives_info( + repository='repo', storage_config={}, info_arguments=flexmock(archive=None, json=False) + ) def test_display_archives_info_with_log_info_calls_borg_with_info_parameter(): @@ -22,7 +25,9 @@ def test_display_archives_info_with_log_info_calls_borg_with_info_parameter(): INFO_COMMAND + ('--info',), output_log_level=logging.WARNING ) insert_logging_mock(logging.INFO) - module.display_archives_info(repository='repo', storage_config={}) + module.display_archives_info( + repository='repo', storage_config={}, info_arguments=flexmock(archive=None, json=False) + ) def test_display_archives_info_with_log_info_and_json_suppresses_most_borg_output(): @@ -31,7 +36,9 @@ def test_display_archives_info_with_log_info_and_json_suppresses_most_borg_outpu ).and_return('[]') insert_logging_mock(logging.INFO) - json_output = module.display_archives_info(repository='repo', storage_config={}, json=True) + json_output = module.display_archives_info( + repository='repo', storage_config={}, info_arguments=flexmock(archive=None, json=True) + ) assert json_output == '[]' @@ -42,7 +49,9 @@ def test_display_archives_info_with_log_debug_calls_borg_with_debug_parameter(): ) insert_logging_mock(logging.DEBUG) - module.display_archives_info(repository='repo', storage_config={}) + module.display_archives_info( + repository='repo', storage_config={}, info_arguments=flexmock(archive=None, json=False) + ) def test_display_archives_info_with_log_debug_and_json_suppresses_most_borg_output(): @@ -51,7 +60,9 @@ def test_display_archives_info_with_log_debug_and_json_suppresses_most_borg_outp ).and_return('[]') insert_logging_mock(logging.DEBUG) - json_output = module.display_archives_info(repository='repo', storage_config={}, json=True) + json_output = module.display_archives_info( + repository='repo', storage_config={}, info_arguments=flexmock(archive=None, json=True) + ) assert json_output == '[]' @@ -61,17 +72,34 @@ def test_display_archives_info_with_json_calls_borg_with_json_parameter(): INFO_COMMAND + ('--json',), output_log_level=None ).and_return('[]') - json_output = module.display_archives_info(repository='repo', storage_config={}, json=True) + json_output = module.display_archives_info( + repository='repo', storage_config={}, info_arguments=flexmock(archive=None, json=True) + ) assert json_output == '[]' +def test_display_archives_info_with_archive_calls_borg_with_archive_parameter(): + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'info', 'repo::archive'), output_log_level=logging.WARNING + ) + + module.display_archives_info( + repository='repo', storage_config={}, info_arguments=flexmock(archive='archive', json=False) + ) + + def test_display_archives_info_with_local_path_calls_borg_via_local_path(): flexmock(module).should_receive('execute_command').with_args( ('borg1',) + INFO_COMMAND[1:], output_log_level=logging.WARNING ) - module.display_archives_info(repository='repo', storage_config={}, local_path='borg1') + module.display_archives_info( + repository='repo', + storage_config={}, + info_arguments=flexmock(archive=None, json=False), + local_path='borg1', + ) def test_display_archives_info_with_remote_path_calls_borg_with_remote_path_parameters(): @@ -79,7 +107,12 @@ def test_display_archives_info_with_remote_path_calls_borg_with_remote_path_para INFO_COMMAND + ('--remote-path', 'borg1'), output_log_level=logging.WARNING ) - module.display_archives_info(repository='repo', storage_config={}, remote_path='borg1') + module.display_archives_info( + repository='repo', + storage_config={}, + info_arguments=flexmock(archive=None, json=False), + remote_path='borg1', + ) def test_display_archives_info_with_lock_wait_calls_borg_with_lock_wait_parameters(): @@ -88,4 +121,22 @@ def test_display_archives_info_with_lock_wait_calls_borg_with_lock_wait_paramete INFO_COMMAND + ('--lock-wait', '5'), output_log_level=logging.WARNING ) - module.display_archives_info(repository='repo', storage_config=storage_config) + module.display_archives_info( + repository='repo', + storage_config=storage_config, + info_arguments=flexmock(archive=None, json=False), + ) + + +@pytest.mark.parametrize('argument_name', ('prefix', 'glob_archives', 'sort_by', 'first', 'last')) +def test_display_archives_info_passes_through_arguments_to_borg(argument_name): + flexmock(module).should_receive('execute_command').with_args( + INFO_COMMAND + ('--' + argument_name.replace('_', '-'), 'value'), + output_log_level=logging.WARNING, + ) + + module.display_archives_info( + repository='repo', + storage_config={}, + info_arguments=flexmock(archive=None, json=False, **{argument_name: 'value'}), + )