From f6b9276de93339aed03ce14bf3b07ab103fd0352 Mon Sep 17 00:00:00 2001 From: Frank Sachsenheim Date: Sat, 13 Aug 2016 14:29:23 +0200 Subject: [PATCH 1/7] Adds arguments to filter archives These are: --sort-by, --first and --last Includes a method to obtain a list of archive infos filtered by these Archives.list: - ensure reverse is always applied - always return a list --- src/borg/archiver.py | 40 +++++++++++++++++++++++++++++++++++++++- src/borg/helpers.py | 20 +++++++++++++++++--- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 3abe7a506..50540369c 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -16,6 +16,7 @@ import traceback from binascii import unhexlify from datetime import datetime from itertools import zip_longest +from operator import attrgetter from .logger import create_logger, setup_logging logger = create_logger() @@ -28,7 +29,8 @@ from .cache import Cache from .constants import * # NOQA from .helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR from .helpers import Error, NoManifestError -from .helpers import location_validator, archivename_validator, ChunkerParams, CompressionSpec, PrefixSpec +from .helpers import location_validator, archivename_validator, ChunkerParams, CompressionSpec +from .helpers import PrefixSpec, sort_by_spec, HUMAN_SORT_KEYS from .helpers import BaseFormatter, ItemFormatter, ArchiveFormatter, format_time, format_file_size, format_archive from .helpers import safe_encode, remove_surrogates, bin_to_hex from .helpers import prune_within, prune_split @@ -2549,6 +2551,23 @@ class Archiver: return parser + @staticmethod + def add_archives_filters_args(subparser): + filters_group = subparser.add_argument_group('filters', 'Archive filters can be applied to repository targets.') + filters_group.add_argument('-P', '--prefix', dest='prefix', type=prefix_spec, default='', + help='only consider archive names starting with this prefix') + + sort_by_default = 'timestamp' + filters_group.add_argument('--sort-by', dest='sort_by', type=sort_by_spec, default=sort_by_default, + help='Comma-separated list of sorting keys; valid keys are: {}; default is: {}' + .format(', '.join(HUMAN_SORT_KEYS), sort_by_default)) + + group = filters_group.add_mutually_exclusive_group() + group.add_argument('--first', dest='first', metavar='N', default=0, type=int, + help='select first N archives') + group.add_argument('--last', dest='last', metavar='N', default=0, type=int, + help='delete last N archives') + def get_args(self, argv, cmd): """usually, just returns argv, except if we deal with a ssh forced command for borg serve.""" result = self.parse_args(argv[1:]) @@ -2611,6 +2630,25 @@ class Archiver: logger.warning("Using a pure-python msgpack! This will result in lower performance.") return args.func(args) + def _get_filtered_archives(self, args, manifest): + if args.location.archive: + raise Error('The options --first, --last and --prefix can only be used on repository targets.') + + archives = manifest.archives.list() + if not archives: + logger.critical('There are no archives.') + self.exit_code = self.exit_code or EXIT_WARNING + return [] + + for sortkey in reversed(args.sort_by.split(',')): + archives.sort(key=attrgetter(sortkey)) + if args.last: + archives.reverse() + + n = args.first or args.last + + return archives[:n] + def sig_info_handler(sig_no, stack): # pragma: no cover """search the stack for infos about the currently processed file and print them""" diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 6d6b8c7e7..85959e864 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -143,10 +143,14 @@ class Archives(abc.MutableMapping): del self._archives[name] def list(self, sort_by=None, reverse=False): - # inexpensive Archive.list_archives replacement if we just need .name, .id, .ts - archives = self.values() # [self[name] for name in self] + """ Inexpensive Archive.list_archives replacement if we just need .name, .id, .ts + Returns list of borg.helpers.ArchiveInfo instances + """ + archives = list(self.values()) # [self[name] for name in self] if sort_by is not None: - archives = sorted(archives, key=attrgetter(sort_by), reverse=reverse) + archives = sorted(archives, key=attrgetter(sort_by)) + if reverse: + archives.reverse() return archives def set_raw_dict(self, d): @@ -655,6 +659,16 @@ def replace_placeholders(text): return format_line(text, data) +HUMAN_SORT_KEYS = ['timestamp'] + list(ArchiveInfo._fields) +HUMAN_SORT_KEYS.remove('ts') + +def sort_by_spec(text): + for token in text.split(','): + if token not in HUMAN_SORT_KEYS: + raise ValueError('Invalid sort key: %s' % token) + return text.replace('timestamp', 'ts') + + def safe_timestamp(item_timestamp_ns): try: return datetime.fromtimestamp(bigint_to_int(item_timestamp_ns) / 1e9) From 17f23639359f3a8207b85d3508d548bd871b8dad Mon Sep 17 00:00:00 2001 From: Frank Sachsenheim Date: Mon, 22 Aug 2016 21:30:38 +0200 Subject: [PATCH 2/7] Adds --prefix to the archives filters arguments - adds prefix argument to helpers.Archives.list - also renames function PrefixSpec to prefix_spec --- src/borg/archiver.py | 12 ++++++------ src/borg/helpers.py | 11 +++++------ 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 50540369c..2604818d8 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -30,7 +30,7 @@ from .constants import * # NOQA from .helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR from .helpers import Error, NoManifestError from .helpers import location_validator, archivename_validator, ChunkerParams, CompressionSpec -from .helpers import PrefixSpec, sort_by_spec, HUMAN_SORT_KEYS +from .helpers import prefix_spec, sort_by_spec, HUMAN_SORT_KEYS from .helpers import BaseFormatter, ItemFormatter, ArchiveFormatter, format_time, format_file_size, format_archive from .helpers import safe_encode, remove_surrogates, bin_to_hex from .helpers import prune_within, prune_split @@ -1605,7 +1605,7 @@ class Archiver: subparser.add_argument('--last', dest='last', type=int, default=None, metavar='N', help='only check last N archives (Default: all)') - subparser.add_argument('-P', '--prefix', dest='prefix', type=PrefixSpec, + subparser.add_argument('-P', '--prefix', dest='prefix', type=prefix_spec, help='only consider archive names starting with this prefix') subparser.add_argument('-p', '--progress', dest='progress', action='store_true', default=False, @@ -1995,7 +1995,7 @@ class Archiver: subparser.add_argument('--format', '--list-format', dest='format', type=str, help="""specify format for file listing (default: "{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NL}")""") - subparser.add_argument('-P', '--prefix', dest='prefix', type=PrefixSpec, + subparser.add_argument('-P', '--prefix', dest='prefix', type=prefix_spec, help='only consider archive names starting with this prefix') subparser.add_argument('-e', '--exclude', dest='excludes', type=parse_pattern, action='append', @@ -2161,7 +2161,7 @@ class Archiver: help='number of monthly archives to keep') subparser.add_argument('-y', '--keep-yearly', dest='yearly', type=int, default=0, help='number of yearly archives to keep') - subparser.add_argument('-P', '--prefix', dest='prefix', type=PrefixSpec, + subparser.add_argument('-P', '--prefix', dest='prefix', type=prefix_spec, help='only consider archive names starting with this prefix') subparser.add_argument('--save-space', dest='save_space', action='store_true', default=False, @@ -2634,7 +2634,7 @@ class Archiver: if args.location.archive: raise Error('The options --first, --last and --prefix can only be used on repository targets.') - archives = manifest.archives.list() + archives = manifest.archives.list(prefix=args.prefix) if not archives: logger.critical('There are no archives.') self.exit_code = self.exit_code or EXIT_WARNING @@ -2645,7 +2645,7 @@ class Archiver: if args.last: archives.reverse() - n = args.first or args.last + n = args.first or args.last or len(archives) return archives[:n] diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 85959e864..27ce121b9 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -142,11 +142,11 @@ class Archives(abc.MutableMapping): name = safe_encode(name) del self._archives[name] - def list(self, sort_by=None, reverse=False): + def list(self, sort_by=None, reverse=False, prefix=''): """ Inexpensive Archive.list_archives replacement if we just need .name, .id, .ts Returns list of borg.helpers.ArchiveInfo instances """ - archives = list(self.values()) # [self[name] for name in self] + archives = [x for x in self.values() if x.name.startswith(prefix)] if sort_by is not None: archives = sorted(archives, key=attrgetter(sort_by)) if reverse: @@ -572,10 +572,6 @@ def CompressionSpec(s): raise ValueError -def PrefixSpec(s): - return replace_placeholders(s) - - def dir_is_cachedir(path): """Determines whether the specified path is a cache directory (and therefore should potentially be excluded from the backup) according to @@ -658,10 +654,13 @@ def replace_placeholders(text): } return format_line(text, data) +prefix_spec = replace_placeholders + HUMAN_SORT_KEYS = ['timestamp'] + list(ArchiveInfo._fields) HUMAN_SORT_KEYS.remove('ts') + def sort_by_spec(text): for token in text.split(','): if token not in HUMAN_SORT_KEYS: From f2d4d36ceae8ebceaeecd06ebdcd9fcfa6348531 Mon Sep 17 00:00:00 2001 From: Frank Sachsenheim Date: Sun, 24 Jul 2016 00:40:15 +0200 Subject: [PATCH 3/7] 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 @@ class Archiver: @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 @@ class Archiver: 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 @@ class Archiver: 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 @@ class ArchiverTestCase(ArchiverTestCaseBase): 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 @@ class ArchiverTestCase(ArchiverTestCaseBase): 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') From 369d0a0881a9c457d4e77d0b9de1cf6b9c57722b Mon Sep 17 00:00:00 2001 From: Frank Sachsenheim Date: Sat, 13 Aug 2016 14:29:23 +0200 Subject: [PATCH 4/7] Adds archives filters for info --- src/borg/archiver.py | 28 ++++++++++++++++++++++++---- src/borg/testsuite/archiver.py | 2 ++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 6efd81ebd..b36dc28f2 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -913,11 +913,24 @@ class Archiver: @with_repository(cache=True) def do_info(self, args, repository, manifest, key, cache): """Show archive details such as disk space used""" + if any((args.location.archive, args.first, args.last, args.prefix)): + return self._info_archives(args, repository, manifest, key, cache) + else: + return self._info_repository(cache) + + def _info_archives(self, args, repository, manifest, key, cache): def format_cmdline(cmdline): return remove_surrogates(' '.join(shlex.quote(x) for x in cmdline)) if args.location.archive: - 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 + + for i, archive_name in enumerate(archive_names, 1): + archive = Archive(repository, key, manifest, archive_name, cache=cache, consider_part_files=args.consider_part_files) stats = archive.calc_stats(cache) print('Archive name: %s' % archive.name) @@ -934,9 +947,15 @@ class Archiver: print(STATS_HEADER) print(str(stats)) print(str(cache)) - else: - print(STATS_HEADER) - print(str(cache)) + if self.exit_code: + break + if len(archive_names) - i: + print() + return self.exit_code + + def _info_repository(self, cache): + print(STATS_HEADER) + print(str(cache)) return self.exit_code @with_repository(exclusive=True) @@ -2102,6 +2121,7 @@ class Archiver: subparser.add_argument('location', metavar='REPOSITORY_OR_ARCHIVE', type=location_validator(), help='archive or repository to display information about') + self.add_archives_filters_args(subparser) break_lock_epilog = textwrap.dedent(""" This command breaks the repository and cache locks. diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 92abd018c..68e9e7b72 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -964,6 +964,8 @@ class ArchiverTestCase(ArchiverTestCaseBase): assert 'All archives:' in info_repo info_archive = self.cmd('info', self.repository_location + '::test') assert 'Archive name: test\n' in info_archive + info_archive = self.cmd('info', '--first', '1', self.repository_location) + assert 'Archive name: test\n' in info_archive def test_comment(self): self.create_regular_file('file1', size=1024 * 80) From e0e9edfb4292f566c452dc881219c28fe840bd82 Mon Sep 17 00:00:00 2001 From: Frank Sachsenheim Date: Sat, 13 Aug 2016 14:29:23 +0200 Subject: [PATCH 5/7] Adds archives filters for list --- src/borg/archiver.py | 51 +++++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index b36dc28f2..39bc37ce9 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -879,34 +879,38 @@ class Archiver: write = sys.stdout.buffer.write if args.location.archive: - matcher, _ = self.build_matcher(args.excludes, args.paths) - with Cache(repository, key, manifest, lock_wait=self.lock_wait) as cache: - archive = Archive(repository, key, manifest, args.location.archive, cache=cache, - consider_part_files=args.consider_part_files) - - if args.format is not None: - format = args.format - elif args.short: - format = "{path}{NL}" - else: - format = "{mode} {user:6} {group:6} {size:8} {isomtime} {path}{extra}{NL}" - formatter = ItemFormatter(archive, format) - - for item in archive.iter_items(lambda item: matcher.match(item.path)): - write(safe_encode(formatter.format_item(item))) + return self._list_archive(args, repository, manifest, key, write) else: + return self._list_repository(args, manifest, write) + + def _list_archive(self, args, repository, manifest, key, write): + matcher, _ = self.build_matcher(args.excludes, args.paths) + with Cache(repository, key, manifest, lock_wait=self.lock_wait) as cache: + archive = Archive(repository, key, manifest, args.location.archive, cache=cache, + consider_part_files=args.consider_part_files) if args.format is not None: format = args.format elif args.short: - format = "{archive}{NL}" + format = "{path}{NL}" else: - format = "{archive:<36} {time} [{id}]{NL}" - formatter = ArchiveFormatter(format) + format = "{mode} {user:6} {group:6} {size:8} {isomtime} {path}{extra}{NL}" + formatter = ItemFormatter(archive, format) - for archive_info in manifest.archives.list(sort_by='ts'): - if args.prefix and not archive_info.name.startswith(args.prefix): - continue - write(safe_encode(formatter.format_item(archive_info))) + for item in archive.iter_items(lambda item: matcher.match(item.path)): + write(safe_encode(formatter.format_item(item))) + return self.exit_code + + def _list_repository(self, args, manifest, write): + if args.format is not None: + format = args.format + elif args.short: + format = "{archive}{NL}" + else: + format = "{archive:<36} {time} [{id}]{NL}" + formatter = ArchiveFormatter(format) + + for archive_info in self._get_filtered_archives(args, manifest): + write(safe_encode(formatter.format_item(archive_info))) return self.exit_code @@ -2043,8 +2047,6 @@ class Archiver: subparser.add_argument('--format', '--list-format', dest='format', type=str, help="""specify format for file listing (default: "{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NL}")""") - subparser.add_argument('-P', '--prefix', dest='prefix', type=prefix_spec, - help='only consider archive names starting with this prefix') subparser.add_argument('-e', '--exclude', dest='excludes', type=parse_pattern, action='append', metavar="PATTERN", help='exclude paths matching PATTERN') @@ -2056,6 +2058,7 @@ class Archiver: help='repository/archive to list contents of') subparser.add_argument('paths', metavar='PATH', nargs='*', type=str, help='paths to list; patterns are supported') + self.add_archives_filters_args(subparser) mount_epilog = textwrap.dedent(""" This command mounts an archive as a FUSE filesystem. This can be useful for From bd7cc38d6e567c4717f9a35a2331fb57dc91b809 Mon Sep 17 00:00:00 2001 From: Frank Sachsenheim Date: Thu, 1 Sep 2016 17:36:37 +0200 Subject: [PATCH 6/7] Changes arg processor names to camelcase --- src/borg/archiver.py | 18 +++++++++--------- src/borg/helpers.py | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 39bc37ce9..66a2e62d5 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -30,7 +30,7 @@ from .constants import * # NOQA from .helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR from .helpers import Error, NoManifestError from .helpers import location_validator, archivename_validator, ChunkerParams, CompressionSpec -from .helpers import prefix_spec, sort_by_spec, HUMAN_SORT_KEYS +from .helpers import PrefixSpec, SortBySpec, HUMAN_SORT_KEYS from .helpers import BaseFormatter, ItemFormatter, ArchiveFormatter, format_time, format_file_size, format_archive from .helpers import safe_encode, remove_surrogates, bin_to_hex from .helpers import prune_within, prune_split @@ -1656,7 +1656,7 @@ class Archiver: subparser.add_argument('--last', dest='last', type=int, default=None, metavar='N', help='only check last N archives (Default: all)') - subparser.add_argument('-P', '--prefix', dest='prefix', type=prefix_spec, + subparser.add_argument('-P', '--prefix', dest='prefix', type=PrefixSpec, help='only consider archive names starting with this prefix') subparser.add_argument('-p', '--progress', dest='progress', action='store_true', default=False, @@ -2213,7 +2213,7 @@ class Archiver: help='number of monthly archives to keep') subparser.add_argument('-y', '--keep-yearly', dest='yearly', type=int, default=0, help='number of yearly archives to keep') - subparser.add_argument('-P', '--prefix', dest='prefix', type=prefix_spec, + subparser.add_argument('-P', '--prefix', dest='prefix', type=PrefixSpec, help='only consider archive names starting with this prefix') subparser.add_argument('--save-space', dest='save_space', action='store_true', default=False, @@ -2606,19 +2606,19 @@ class Archiver: @staticmethod def add_archives_filters_args(subparser): filters_group = subparser.add_argument_group('filters', 'Archive filters can be applied to repository targets.') - filters_group.add_argument('-P', '--prefix', dest='prefix', type=prefix_spec, default='', + filters_group.add_argument('-P', '--prefix', dest='prefix', type=PrefixSpec, default='', help='only consider archive names starting with this prefix') sort_by_default = 'timestamp' - filters_group.add_argument('--sort-by', dest='sort_by', type=sort_by_spec, default=sort_by_default, - help='Comma-separated list of sorting keys; valid keys are: {}; default is: {}' - .format(', '.join(HUMAN_SORT_KEYS), sort_by_default)) + filters_group.add_argument('--sort-by', dest='sort_by', type=SortBySpec, default=sort_by_default, + help='Comma-separated list of sorting keys; valid keys are: {}; default is: {}' + .format(', '.join(HUMAN_SORT_KEYS), sort_by_default)) group = filters_group.add_mutually_exclusive_group() group.add_argument('--first', dest='first', metavar='N', default=0, type=int, - help='select first N archives') + help='consider first N archives after other filter args were applied') group.add_argument('--last', dest='last', metavar='N', default=0, type=int, - help='delete last N archives') + help='consider last N archives after other filter args were applied') def get_args(self, argv, cmd): """usually, just returns argv, except if we deal with a ssh forced command for borg serve.""" diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 27ce121b9..541c832eb 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -654,14 +654,14 @@ def replace_placeholders(text): } return format_line(text, data) -prefix_spec = replace_placeholders +PrefixSpec = replace_placeholders HUMAN_SORT_KEYS = ['timestamp'] + list(ArchiveInfo._fields) HUMAN_SORT_KEYS.remove('ts') -def sort_by_spec(text): +def SortBySpec(text): for token in text.split(','): if token not in HUMAN_SORT_KEYS: raise ValueError('Invalid sort key: %s' % token) From 089995ef73a990e89cf3e65ca7e345dbbf52ab30 Mon Sep 17 00:00:00 2001 From: Frank Sachsenheim Date: Fri, 30 Sep 2016 20:01:59 +0200 Subject: [PATCH 7/7] Changes on filters after feedback --- src/borg/archiver.py | 8 ++------ src/borg/helpers.py | 5 +++-- src/borg/testsuite/archiver.py | 1 - 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 66a2e62d5..31b1f59dd 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -2616,9 +2616,9 @@ class Archiver: group = filters_group.add_mutually_exclusive_group() group.add_argument('--first', dest='first', metavar='N', default=0, type=int, - help='consider first N archives after other filter args were applied') + help='consider first N archives after other filters were applied') group.add_argument('--last', dest='last', metavar='N', default=0, type=int, - help='consider last N archives after other filter args were applied') + help='consider last N archives after other filters were applied') def get_args(self, argv, cmd): """usually, just returns argv, except if we deal with a ssh forced command for borg serve.""" @@ -2687,10 +2687,6 @@ class Archiver: raise Error('The options --first, --last and --prefix can only be used on repository targets.') archives = manifest.archives.list(prefix=args.prefix) - if not archives: - logger.critical('There are no archives.') - self.exit_code = self.exit_code or EXIT_WARNING - return [] for sortkey in reversed(args.sort_by.split(',')): archives.sort(key=attrgetter(sortkey)) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 541c832eb..4c6b939cf 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -143,8 +143,9 @@ class Archives(abc.MutableMapping): del self._archives[name] def list(self, sort_by=None, reverse=False, prefix=''): - """ Inexpensive Archive.list_archives replacement if we just need .name, .id, .ts - Returns list of borg.helpers.ArchiveInfo instances + """ + Inexpensive Archive.list_archives replacement if we just need .name, .id, .ts + Returns list of borg.helpers.ArchiveInfo instances """ archives = [x for x in self.values() if x.name.startswith(prefix)] if sort_by is not None: diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 68e9e7b72..cb79d6b24 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1826,7 +1826,6 @@ class ArchiverTestCase(ArchiverTestCaseBase): 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')