diff --git a/src/borg/archive.py b/src/borg/archive.py index 1ac2133e7..4e70c8c41 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -714,7 +714,7 @@ Utilization of max. archive size: {csize_max:.0%} raise ChunksIndexError(cid) except Repository.ObjectNotFound as e: # object not in repo - strange, but we wanted to delete it anyway. - if not forced: + if forced == 0: raise error = True @@ -738,14 +738,14 @@ Utilization of max. archive size: {csize_max:.0%} except (TypeError, ValueError): # if items metadata spans multiple chunks and one chunk got dropped somehow, # it could be that unpacker yields bad types - if not forced: + if forced == 0: raise error = True if progress: pi.finish() except (msgpack.UnpackException, Repository.ObjectNotFound): # items metadata corrupted - if not forced: + if forced == 0: raise error = True # in forced delete mode, we try hard to delete at least the manifest entry, diff --git a/src/borg/archiver.py b/src/borg/archiver.py index b41df11ad..f81c583cb 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -844,6 +844,26 @@ class Archiver: if not archive_names: return self.exit_code + if args.forced == 2: + deleted = False + for i, archive_name in enumerate(archive_names, 1): + try: + del manifest.archives[archive_name] + except KeyError: + self.exit_code = EXIT_WARNING + logger.warning('Archive {} not found ({}/{}).'.format(archive_name, i, len(archive_names))) + else: + deleted = True + logger.info('Deleted {} ({}/{}).'.format(archive_name, i, len(archive_names))) + if deleted: + manifest.write() + # note: might crash in compact() after committing the repo + repository.commit() + logger.info('Done. Run "borg check --repair" to clean up the mess.') + else: + logger.warning('Aborted.') + return self.exit_code + stats_logger = logging.getLogger('borg.output.stats') if args.stats: log_multi(DASHES, STATS_HEADER, logger=stats_logger) @@ -861,7 +881,7 @@ class Archiver: if args.stats: log_multi(stats.summary.format(label='Deleted data:', stats=stats), DASHES, logger=stats_logger) - if not args.forced and self.exit_code: + if args.forced == 0 and self.exit_code: break if args.stats: stats_logger.info(str(cache)) @@ -2450,8 +2470,9 @@ class Archiver: action='store_true', default=False, help='delete only the local cache for the given repository') subparser.add_argument('--force', dest='forced', - action='store_true', default=False, - help='force deletion of corrupted archives') + action='count', default=0, + help='force deletion of corrupted archives, ' + 'use --force --force in case --force does not work.') subparser.add_argument('--save-space', dest='save_space', action='store_true', default=False, help='work slower, but using less space') diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index ca0f41028..03f4a6c93 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1193,6 +1193,20 @@ class ArchiverTestCase(ArchiverTestCaseBase): # Make sure the repo is gone self.assertFalse(os.path.exists(self.repository_path)) + def test_delete_double_force(self): + self.cmd('init', '--encryption=none', self.repository_location) + self.create_src_archive('test') + with Repository(self.repository_path, exclusive=True) as repository: + manifest, key = Manifest.load(repository) + archive = Archive(repository, key, manifest, 'test') + id = archive.metadata.items[0] + repository.put(id, b'corrupted items metadata stream chunk') + repository.commit() + self.cmd('delete', '--force', '--force', self.repository_location + '::test') + self.cmd('check', '--repair', self.repository_location) + output = self.cmd('list', self.repository_location) + self.assert_not_in('test', output) + def test_corrupted_repository(self): self.cmd('init', '--encryption=repokey', self.repository_location) self.create_src_archive('test')