diff --git a/src/borg/archiver/__init__.py b/src/borg/archiver/__init__.py index 6111199c4..c9e1236fa 100644 --- a/src/borg/archiver/__init__.py +++ b/src/borg/archiver/__init__.py @@ -418,10 +418,14 @@ class Archiver( replace_placeholders.override("now", DatetimeWrapper(args.timestamp)) replace_placeholders.override("utcnow", DatetimeWrapper(args.timestamp.astimezone(timezone.utc))) args.location = args.location.with_timestamp(args.timestamp) - for name in "name", "other_name", "newname", "match_archives", "comment": + for name in "name", "other_name", "newname", "comment": value = getattr(args, name, None) if value is not None: setattr(args, name, replace_placeholders(value)) + for name in ("match_archives",): # lists + value = getattr(args, name, None) + if value: + setattr(args, name, [replace_placeholders(elem) for elem in value]) return args diff --git a/src/borg/archiver/_common.py b/src/borg/archiver/_common.py index 3b9453b12..f8223eb73 100644 --- a/src/borg/archiver/_common.py +++ b/src/borg/archiver/_common.py @@ -256,7 +256,7 @@ def with_archive(method): def wrapper(self, args, repository, manifest, **kwargs): archive_name = getattr(args, "name", None) assert archive_name is not None - archive_info = manifest.archives.get_one(archive_name) + archive_info = manifest.archives.get_one([archive_name]) archive = Archive( manifest, archive_info.id, @@ -379,8 +379,8 @@ def define_archive_filters_group(subparser, *, sort_by=True, first_last=True, ol "--match-archives", metavar="PATTERN", dest="match_archives", - action=Highlander, - help='only consider archive names matching the pattern. see "borg help match-archives".', + action="append", + help='only consider archives matching all patterns. see "borg help match-archives".', ) if sort_by: diff --git a/src/borg/archiver/debug_cmd.py b/src/borg/archiver/debug_cmd.py index b3ddb6bfc..cac736c56 100644 --- a/src/borg/archiver/debug_cmd.py +++ b/src/borg/archiver/debug_cmd.py @@ -32,7 +32,7 @@ class DebugMixIn: def do_debug_dump_archive_items(self, args, repository, manifest): """dump (decrypted, decompressed) archive items metadata (not: data)""" repo_objs = manifest.repo_objs - archive_info = manifest.archives.get_one(args.name) + archive_info = manifest.archives.get_one([args.name]) archive = Archive(manifest, archive_info.id) for i, item_id in enumerate(archive.metadata.items): _, data = repo_objs.parse(item_id, repository.get(item_id), ro_type=ROBJ_ARCHIVE_STREAM) @@ -45,7 +45,7 @@ class DebugMixIn: @with_repository(compatibility=Manifest.NO_OPERATION_CHECK) def do_debug_dump_archive(self, args, repository, manifest): """dump decoded archive metadata (not: data)""" - archive_info = manifest.archives.get_one(args.name) + archive_info = manifest.archives.get_one([args.name]) repo_objs = manifest.repo_objs try: archive_meta_orig = manifest.archives.get_by_id(archive_info.id, raw=True) diff --git a/src/borg/archiver/delete_cmd.py b/src/borg/archiver/delete_cmd.py index 305682ef7..5ad81f95b 100644 --- a/src/borg/archiver/delete_cmd.py +++ b/src/borg/archiver/delete_cmd.py @@ -19,13 +19,13 @@ class DeleteMixIn: dry_run = args.dry_run manifest = Manifest.load(repository, (Manifest.Operation.DELETE,)) if args.name: - archive_infos = [manifest.archives.get_one(args.name)] + archive_infos = [manifest.archives.get_one([args.name])] else: archive_infos = manifest.archives.list_considering(args) count = len(archive_infos) if count == 0: return - if not args.name and args.match_archives is None and args.first == 0 and args.last == 0: + if not args.name and not args.match_archives and args.first == 0 and args.last == 0: raise CommandError( "Aborting: if you really want to delete all archives, please use -a 'sh:*' " "or just delete the whole repository (might be much faster)." diff --git a/src/borg/archiver/diff_cmd.py b/src/borg/archiver/diff_cmd.py index 52f33f54d..166539368 100644 --- a/src/borg/archiver/diff_cmd.py +++ b/src/borg/archiver/diff_cmd.py @@ -25,8 +25,8 @@ class DiffMixIn: else: format = os.environ.get("BORG_DIFF_FORMAT", "{change} {path}{NL}") - archive1_info = manifest.archives.get_one(args.name) - archive2_info = manifest.archives.get_one(args.other_name) + archive1_info = manifest.archives.get_one([args.name]) + archive2_info = manifest.archives.get_one([args.other_name]) archive1 = Archive(manifest, archive1_info.id) archive2 = Archive(manifest, archive2_info.id) diff --git a/src/borg/archiver/help_cmd.py b/src/borg/archiver/help_cmd.py index f848c27de..82adc9b85 100644 --- a/src/borg/archiver/help_cmd.py +++ b/src/borg/archiver/help_cmd.py @@ -264,10 +264,19 @@ class HelpMixIn: ) helptext["match-archives"] = textwrap.dedent( """ - The ``--match-archives`` option matches a given pattern against the list of all archive - names in the repository. + The ``--match-archives`` option matches a given pattern against the list of all archives + in the repository. It can be given multiple times. - It uses pattern styles similar to the ones described by ``borg help patterns``: + The patterns can have a prefix of: + + - name: pattern match on the archive name (default) + - aid: prefix match on the archive id (only one result allowed) + - user: exact match on the username who created the archive + - host: exact match on the hostname where the archive was created + - tags: match on the archive tags + + In case of a name pattern match, + it uses pattern styles similar to the ones described by ``borg help patterns``: Identical match pattern, selector ``id:`` (default) Simple string match, must fully match exactly as given. @@ -281,16 +290,26 @@ class HelpMixIn: Examples:: - # id: style + # name match, id: style borg delete --match-archives 'id:archive-with-crap' borg delete -a 'id:archive-with-crap' # same, using short option borg delete -a 'archive-with-crap' # same, because 'id:' is the default - # sh: style + # name match, sh: style borg delete -a 'sh:home-kenny-*' - # re: style - borg delete -a 're:pc[123]-home-(user1|user2)-2022-09-.*'\n\n""" + # name match, re: style + borg delete -a 're:pc[123]-home-(user1|user2)-2022-09-.*' + + # archive id prefix match: + borg delete -a 'aid:d34db33f' + + # host or user match + borg delete -a 'user:kenny' + borg delete -a 'host:kenny-pc' + + # tags match + borg delete -a 'tags:TAG1' -a 'tags:TAG2'\n\n""" ) helptext["placeholders"] = textwrap.dedent( """ diff --git a/src/borg/archiver/info_cmd.py b/src/borg/archiver/info_cmd.py index 0afbb0009..0d3ca0581 100644 --- a/src/borg/archiver/info_cmd.py +++ b/src/borg/archiver/info_cmd.py @@ -19,7 +19,7 @@ class InfoMixIn: """Show archive details such as disk space used""" if args.name: - archive_infos = [manifest.archives.get_one(args.name)] + archive_infos = [manifest.archives.get_one([args.name])] else: archive_infos = manifest.archives.list_considering(args) diff --git a/src/borg/archiver/list_cmd.py b/src/borg/archiver/list_cmd.py index bbd6d1825..013e695cd 100644 --- a/src/borg/archiver/list_cmd.py +++ b/src/borg/archiver/list_cmd.py @@ -27,7 +27,7 @@ class ListMixIn: else: format = os.environ.get("BORG_LIST_FORMAT", "{mode} {user:6} {group:6} {size:8} {mtime} {path}{extra}{NL}") - archive_info = manifest.archives.get_one(args.name) + archive_info = manifest.archives.get_one([args.name]) def _list_inner(cache): archive = Archive(manifest, archive_info.id, cache=cache) diff --git a/src/borg/cache.py b/src/borg/cache.py index 70101fcea..b5cd329c7 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -402,7 +402,7 @@ class FilesCacheMixin: from .archive import Archive # get the latest archive with the IDENTICAL name, supporting archive series: - archives = self.manifest.archives.list(match=self.archive_name, sort_by=["ts"], last=1) + archives = self.manifest.archives.list(match=[self.archive_name], sort_by=["ts"], last=1) if not archives: # nothing found return diff --git a/src/borg/manifest.py b/src/borg/manifest.py index 124373390..2bcb6e490 100644 --- a/src/borg/manifest.py +++ b/src/borg/manifest.py @@ -172,29 +172,31 @@ class Archives: host=info["hostname"], ) - def _matching_info_tuples(self, match, match_end): - archive_infos = self._info_tuples() - if match is None: - archive_infos = list(archive_infos) - elif match.startswith("aid:"): # do a match on the archive ID (prefix) - wanted_id = match.removeprefix("aid:") - archive_infos = [x for x in archive_infos if bin_to_hex(x.id).startswith(wanted_id)] - if len(archive_infos) != 1: - raise CommandError("archive ID based match needs to match precisely one archive ID") - elif match.startswith("tags:"): - wanted_tags = match.removeprefix("tags:") - wanted_tags = [tag for tag in wanted_tags.split(",") if tag] # remove empty tags - archive_infos = [x for x in archive_infos if set(x.tags) >= set(wanted_tags)] - elif match.startswith("user:"): - wanted_user = match.removeprefix("user:") - archive_infos = [x for x in archive_infos if x.user == wanted_user] - elif match.startswith("host:"): - wanted_host = match.removeprefix("host:") - archive_infos = [x for x in archive_infos if x.host == wanted_host] - else: # do a match on the name - regex = get_regex_from_pattern(match) - regex = re.compile(regex + match_end) - archive_infos = [x for x in archive_infos if regex.match(x.name) is not None] + def _matching_info_tuples(self, match_patterns, match_end): + archive_infos = list(self._info_tuples()) + if match_patterns: + assert isinstance(match_patterns, list), f"match_pattern is a {type(match_patterns)}" + for match in match_patterns: + if match.startswith("aid:"): # do a match on the archive ID (prefix) + wanted_id = match.removeprefix("aid:") + archive_infos = [x for x in archive_infos if bin_to_hex(x.id).startswith(wanted_id)] + if len(archive_infos) != 1: + raise CommandError("archive ID based match needs to match precisely one archive ID") + elif match.startswith("tags:"): + wanted_tags = match.removeprefix("tags:") + wanted_tags = [tag for tag in wanted_tags.split(",") if tag] # remove empty tags + archive_infos = [x for x in archive_infos if set(x.tags) >= set(wanted_tags)] + elif match.startswith("user:"): + wanted_user = match.removeprefix("user:") + archive_infos = [x for x in archive_infos if x.user == wanted_user] + elif match.startswith("host:"): + wanted_host = match.removeprefix("host:") + archive_infos = [x for x in archive_infos if x.host == wanted_host] + else: # do a match on the name + match = match.removeprefix("name:") # accept optional name: prefix + regex = get_regex_from_pattern(match) + regex = re.compile(regex + match_end) + archive_infos = [x for x in archive_infos if regex.match(x.name) is not None] return archive_infos def count(self): diff --git a/src/borg/testsuite/archiver/__init__.py b/src/borg/testsuite/archiver/__init__.py index cb940c3e4..89500283b 100644 --- a/src/borg/testsuite/archiver/__init__.py +++ b/src/borg/testsuite/archiver/__init__.py @@ -169,7 +169,7 @@ def open_archive(repo_path, name): repository = Repository(repo_path, exclusive=True) with repository: manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) - archive_info = manifest.archives.get_one(name) + archive_info = manifest.archives.get_one([name]) archive = Archive(manifest, archive_info.id) return archive, repository