From f6b9276de93339aed03ce14bf3b07ab103fd0352 Mon Sep 17 00:00:00 2001 From: Frank Sachsenheim Date: Sat, 13 Aug 2016 14:29:23 +0200 Subject: [PATCH] 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)