2
0
Эх сурвалжийг харах

allow -a / --match-archives multiple times, ANDed

e.g.: borg delete -a home -a user:kenny -a host:kenny-pc
Thomas Waldmann 8 сар өмнө
parent
commit
f082df7f33

+ 5 - 1
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
 

+ 3 - 3
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:

+ 2 - 2
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)

+ 2 - 2
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)."

+ 2 - 2
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)
 

+ 26 - 7
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(
         """

+ 1 - 1
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)
 

+ 1 - 1
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)

+ 1 - 1
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

+ 25 - 23
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):

+ 1 - 1
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