diff --git a/NEWS b/NEWS index 09def4055..c3f6a8334 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,8 @@ * #235: Pass extra options directly to particular Borg commands, handy for Borg options that borgmatic does not yet support natively. Use "extra_borg_options" in the storage configuration section. + * #266: Attempt to repair any inconsistencies found during a consistency check via + "borgmatic check --repair" flag. 1.4.16 * #256: Fix for "before_backup" hook not triggering an error when the command contains "borg" and diff --git a/borgmatic/borg/check.py b/borgmatic/borg/check.py index 89c58555c..45e59f28a 100644 --- a/borgmatic/borg/check.py +++ b/borgmatic/borg/check.py @@ -1,7 +1,7 @@ import logging from borgmatic.borg import extract -from borgmatic.execute import execute_command +from borgmatic.execute import execute_command, execute_command_without_capture DEFAULT_CHECKS = ('repository', 'archives') DEFAULT_PREFIX = '{hostname}-' @@ -91,12 +91,13 @@ def check_archives( consistency_config, local_path='borg', remote_path=None, + repair=None, only_checks=None, ): ''' Given a local or remote repository path, a storage config dict, a consistency config dict, - local/remote commands to run, and an optional list of checks to use instead of configured - checks, check the contained Borg archives for consistency. + local/remote commands to run, whether to attempt a repair, and an optional list of checks + to use instead of configured checks, check the contained Borg archives for consistency. If there are no consistency checks to run, skip running them. ''' @@ -106,9 +107,7 @@ def check_archives( extra_borg_options = storage_config.get('extra_borg_options', {}).get('check', '') if set(checks).intersection(set(DEFAULT_CHECKS + ('data',))): - remote_path_flags = ('--remote-path', remote_path) if remote_path else () lock_wait = storage_config.get('lock_wait', None) - lock_wait_flags = ('--lock-wait', str(lock_wait)) if lock_wait else () verbosity_flags = () if logger.isEnabledFor(logging.INFO): @@ -120,14 +119,21 @@ def check_archives( full_command = ( (local_path, 'check') + + (('--repair',) if repair else ()) + _make_check_flags(checks, check_last, prefix) - + remote_path_flags - + lock_wait_flags + + (('--remote-path', remote_path) if remote_path else ()) + + (('--lock-wait', str(lock_wait)) if lock_wait else ()) + verbosity_flags + (tuple(extra_borg_options.split(' ')) if extra_borg_options else ()) + (repository,) ) + # The Borg repair option trigger an interactive prompt, which won't work when output is + # captured. + if repair: + execute_command_without_capture(full_command, error_on_warnings=True) + return + execute_command(full_command, error_on_warnings=True) if 'extract' in checks: diff --git a/borgmatic/commands/arguments.py b/borgmatic/commands/arguments.py index 98d948488..86794642d 100644 --- a/borgmatic/commands/arguments.py +++ b/borgmatic/commands/arguments.py @@ -266,6 +266,13 @@ def parse_arguments(*unparsed_arguments): add_help=False, ) check_group = check_parser.add_argument_group('check arguments') + check_group.add_argument( + '--repair', + dest='repair', + default=False, + action='store_true', + help='Attempt to repair any inconsistencies found (experimental and only for interactive use)', + ) check_group.add_argument( '--only', metavar='CHECK', diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index c33b2483a..0817a97ef 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -230,6 +230,7 @@ def run_actions( consistency, local_path=local_path, remote_path=remote_path, + repair=arguments['check'].repair, only_checks=arguments['check'].only, ) if 'extract' in arguments: diff --git a/tests/unit/borg/test_check.py b/tests/unit/borg/test_check.py index df82936bb..b2506aae0 100644 --- a/tests/unit/borg/test_check.py +++ b/tests/unit/borg/test_check.py @@ -158,6 +158,21 @@ def test_make_check_flags_with_default_checks_and_prefix_includes_prefix_flag(): assert flags == ('--prefix', 'foo-') +def test_check_archives_with_repair_calls_borg_with_repair_parameter(): + checks = ('repository',) + consistency_config = {'check_last': None} + flexmock(module).should_receive('_parse_checks').and_return(checks) + flexmock(module).should_receive('_make_check_flags').and_return(()) + flexmock(module).should_receive('execute_command').never() + flexmock(module).should_receive('execute_command_without_capture').with_args( + ('borg', 'check', '--repair', 'repo'), error_on_warnings=True + ).once() + + module.check_archives( + repository='repo', storage_config={}, consistency_config=consistency_config, repair=True + ) + + @pytest.mark.parametrize( 'checks', (