From f2d4d36ceae8ebceaeecd06ebdcd9fcfa6348531 Mon Sep 17 00:00:00 2001 From: Frank Sachsenheim Date: Sun, 24 Jul 2016 00:40:15 +0200 Subject: [PATCH] Allows delete to be used with archive filters --- src/borg/archiver.py | 91 ++++++++++++++++++++++------------ src/borg/testsuite/archiver.py | 14 ++++++ 2 files changed, 74 insertions(+), 31 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 2604818d8..6efd81ebd 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -770,11 +770,31 @@ def do_rename(self, args, repository, manifest, key, cache, archive): @with_repository(exclusive=True, manifest=False) def do_delete(self, args, repository): - """Delete an existing repository or archive""" + """Delete an existing repository or archives""" + if any((args.location.archive, args.first, args.last, args.prefix)): + return self._delete_archives(args, repository) + else: + return self._delete_repository(args, repository) + + def _delete_archives(self, args, repository): + """Delete archives""" + manifest, key = Manifest.load(repository) + if args.location.archive: - manifest, key = Manifest.load(repository) - with Cache(repository, key, manifest, lock_wait=self.lock_wait) as cache: - archive = Archive(repository, key, manifest, args.location.archive, cache=cache) + archive_names = (args.location.archive,) + else: + archive_names = tuple(x.name for x in self._get_filtered_archives(args, manifest)) + if not archive_names: + return self.exit_code + + stats_logger = logging.getLogger('borg.output.stats') + if args.stats: + log_multi(DASHES, STATS_HEADER, logger=stats_logger) + + with Cache(repository, key, manifest, lock_wait=self.lock_wait) as cache: + for i, archive_name in enumerate(archive_names, 1): + logger.info('Deleting {} ({}/{}):'.format(archive_name, i, len(archive_names))) + archive = Archive(repository, key, manifest, archive_name, cache=cache) stats = Statistics() archive.delete(stats, progress=args.progress, forced=args.forced) manifest.write() @@ -782,33 +802,41 @@ def do_delete(self, args, repository): cache.commit() logger.info("Archive deleted.") if args.stats: - log_multi(DASHES, - STATS_HEADER, - stats.summary.format(label='Deleted data:', stats=stats), - str(cache), - DASHES, logger=logging.getLogger('borg.output.stats')) - else: - if not args.cache_only: - msg = [] - try: - manifest, key = Manifest.load(repository) - except NoManifestError: - msg.append("You requested to completely DELETE the repository *including* all archives it may contain.") - msg.append("This repository seems to have no manifest, so we can't tell anything about its contents.") - else: - msg.append("You requested to completely DELETE the repository *including* all archives it contains:") - for archive_info in manifest.archives.list(sort_by='ts'): - msg.append(format_archive(archive_info)) - msg.append("Type 'YES' if you understand this and want to continue: ") - msg = '\n'.join(msg) - if not yes(msg, false_msg="Aborting.", invalid_msg='Invalid answer, aborting.', truish=('YES', ), - retry=False, env_var_override='BORG_DELETE_I_KNOW_WHAT_I_AM_DOING'): - self.exit_code = EXIT_ERROR - return self.exit_code - repository.destroy() - logger.info("Repository deleted.") - Cache.destroy(repository) - logger.info("Cache deleted.") + log_multi(stats.summary.format(label='Deleted data:', stats=stats), + DASHES, logger=stats_logger) + if not args.forced and self.exit_code: + break + if args.stats: + stats_logger.info(str(cache)) + + return self.exit_code + + def _delete_repository(self, args, repository): + """Delete a repository""" + if not args.cache_only: + msg = [] + try: + manifest, key = Manifest.load(repository) + except NoManifestError: + msg.append("You requested to completely DELETE the repository *including* all archives it may " + "contain.") + msg.append("This repository seems to have no manifest, so we can't tell anything about its " + "contents.") + else: + msg.append("You requested to completely DELETE the repository *including* all archives it " + "contains:") + for archive_info in manifest.archives.list(sort_by='ts'): + msg.append(format_archive(archive_info)) + msg.append("Type 'YES' if you understand this and want to continue: ") + msg = '\n'.join(msg) + if not yes(msg, false_msg="Aborting.", invalid_msg='Invalid answer, aborting.', truish=('YES',), + retry=False, env_var_override='BORG_DELETE_I_KNOW_WHAT_I_AM_DOING'): + self.exit_code = EXIT_ERROR + return self.exit_code + repository.destroy() + logger.info("Repository deleted.") + Cache.destroy(repository) + logger.info("Cache deleted.") return self.exit_code @with_repository() @@ -1969,6 +1997,7 @@ def build_parser(self, prog=None): subparser.add_argument('location', metavar='TARGET', nargs='?', default='', type=location_validator(), help='archive or repository to delete') + self.add_archives_filters_args(subparser) list_epilog = textwrap.dedent(""" This command lists the contents of a repository or an archive. diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 50cb42c5c..92abd018c 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -59,6 +59,9 @@ def exec_cmd(*args, archiver=None, fork=False, exe=None, **kw): except subprocess.CalledProcessError as e: output = e.output ret = e.returncode + except SystemExit as e: # possibly raised by argparse + output = '' + ret = e.code return ret, os.fsdecode(output) else: stdin, stdout, stderr = sys.stdin, sys.stdout, sys.stderr @@ -987,8 +990,13 @@ def test_delete(self): self.cmd('init', self.repository_location) self.cmd('create', self.repository_location + '::test', 'input') self.cmd('create', self.repository_location + '::test.2', 'input') + self.cmd('create', self.repository_location + '::test.3', 'input') + self.cmd('create', self.repository_location + '::another_test.1', 'input') + self.cmd('create', self.repository_location + '::another_test.2', 'input') self.cmd('extract', '--dry-run', self.repository_location + '::test') self.cmd('extract', '--dry-run', self.repository_location + '::test.2') + self.cmd('delete', '--prefix', 'another_', self.repository_location) + self.cmd('delete', '--last', '1', self.repository_location) self.cmd('delete', self.repository_location + '::test') self.cmd('extract', '--dry-run', self.repository_location + '::test.2') output = self.cmd('delete', '--stats', self.repository_location + '::test.2') @@ -1811,6 +1819,12 @@ def test_recreate_list_output(self): self.assert_not_in("input/file1", output) self.assert_not_in("x input/file5", output) + def test_bad_filters(self): + self.cmd('init', self.repository_location) + self.cmd('create', self.repository_location + '::test', 'input') + self.cmd('delete', '--first', '1', '--last', '1', self.repository_location, fork=True, exit_code=2) + + def test_key_export_keyfile(self): export_file = self.output_path + '/exported' self.cmd('init', self.repository_location, '--encryption', 'keyfile')