diff --git a/NEWS b/NEWS index 32a3f0c6..f826d040 100644 --- a/NEWS +++ b/NEWS @@ -1,8 +1,8 @@ 2.0.2.dev0 * #1035: Document potential performance issues and workarounds with the ZFS, Btrfs, and LVM hooks: https://torsion.org/borgmatic/docs/how-to/snapshot-your-filesystems/ - * #1053: Display a nicer error message when running the "recreate" action on a leftover temporary - archive from a prior recreate run. + * #1053: Display a nicer error message when the "recreate" action encounters an archive that + already exists. * #1059: Fix a regression in which soft failure exit codes in command hooks were not respected. * #1060: Fix action command hooks getting run too many times when multiple borgmatic actions are executed (implicitly or explicitly). diff --git a/borgmatic/actions/recreate.py b/borgmatic/actions/recreate.py index ad7b041e..822a57c4 100644 --- a/borgmatic/actions/recreate.py +++ b/borgmatic/actions/recreate.py @@ -1,5 +1,7 @@ import logging +import subprocess +import borgmatic.borg.info import borgmatic.borg.recreate import borgmatic.borg.repo_list import borgmatic.config.validate @@ -8,6 +10,9 @@ from borgmatic.actions.pattern import collect_patterns, process_patterns logger = logging.getLogger(__name__) +BORG_EXIT_CODE_ARCHIVE_ALREADY_EXISTS = 30 + + def run_recreate( repository, config, @@ -53,14 +58,27 @@ def run_recreate( f'The archive "{recreate_arguments.archive}" is leftover from a prior recreate. Select a different archive.' ) - borgmatic.borg.recreate.recreate_archive( - repository['path'], - archive, - config, - local_borg_version, - recreate_arguments, - global_arguments, - local_path=local_path, - remote_path=remote_path, - patterns=processed_patterns, - ) + try: + borgmatic.borg.recreate.recreate_archive( + repository['path'], + archive, + config, + local_borg_version, + recreate_arguments, + global_arguments, + local_path=local_path, + remote_path=remote_path, + patterns=processed_patterns, + ) + except subprocess.CalledProcessError as error: + if error.returncode == BORG_EXIT_CODE_ARCHIVE_ALREADY_EXISTS: + if recreate_arguments.target: + raise ValueError( + f'The archive "{recreate_arguments.target}" already exists. Delete it first or set a different target archive name.' + ) + elif archive: + raise ValueError( + f'The archive "{archive}.recreate" is leftover from a prior recreate. Delete it first or select a different archive.' + ) + + raise diff --git a/tests/unit/actions/test_recreate.py b/tests/unit/actions/test_recreate.py index f19bfbe3..af19b70b 100644 --- a/tests/unit/actions/test_recreate.py +++ b/tests/unit/actions/test_recreate.py @@ -92,3 +92,91 @@ def test_run_recreate_with_latest_archive_resolving_to_leftover_recreate_archive local_path=None, remote_path=None, ) + + +def test_run_recreate_with_archive_already_exists_error_raises(): + flexmock(module.logger).answer = lambda message: None + flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True) + flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return( + flexmock() + ) + flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name').and_return( + 'test-archive' + ) + flexmock(module.borgmatic.borg.recreate).should_receive('recreate_archive').and_raise( + module.subprocess.CalledProcessError( + returncode=module.BORG_EXIT_CODE_ARCHIVE_ALREADY_EXISTS, + cmd='borg recreate or whatever', + ) + ) + + with pytest.raises(ValueError): + module.run_recreate( + repository={'path': 'repo'}, + config={}, + local_borg_version=None, + recreate_arguments=flexmock(repository=flexmock(), archive='test-archive', target=None), + global_arguments=flexmock(), + local_path=None, + remote_path=None, + ) + + +def test_run_recreate_with_target_and_archive_already_exists_error_raises(): + flexmock(module.logger).answer = lambda message: None + flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True) + flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return( + flexmock() + ) + flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name').and_return( + 'test-archive' + ) + flexmock(module.borgmatic.borg.recreate).should_receive('recreate_archive').and_raise( + module.subprocess.CalledProcessError( + returncode=module.BORG_EXIT_CODE_ARCHIVE_ALREADY_EXISTS, + cmd='borg recreate or whatever', + ) + ) + + with pytest.raises(ValueError): + module.run_recreate( + repository={'path': 'repo'}, + config={}, + local_borg_version=None, + recreate_arguments=flexmock( + repository=flexmock(), archive='test-archive', target='target-archive' + ), + global_arguments=flexmock(), + local_path=None, + remote_path=None, + ) + + +def test_run_recreate_with_other_called_process_error_passes_it_through(): + flexmock(module.logger).answer = lambda message: None + flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True) + flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return( + flexmock() + ) + flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name').and_return( + 'test-archive' + ) + flexmock(module.borgmatic.borg.recreate).should_receive('recreate_archive').and_raise( + module.subprocess.CalledProcessError( + returncode=1, + cmd='borg recreate or whatever', + ) + ) + + with pytest.raises(module.subprocess.CalledProcessError): + module.run_recreate( + repository={'path': 'repo'}, + config={}, + local_borg_version=None, + recreate_arguments=flexmock( + repository=flexmock(), archive='test-archive', target='target-archive' + ), + global_arguments=flexmock(), + local_path=None, + remote_path=None, + )