mirror of
https://github.com/borgbackup/borg.git
synced 2026-06-11 09:59:19 -04:00
allow -a / --match-archives multiple times, ANDed
e.g.: borg delete -a home -a user:kenny -a host:kenny-pc
This commit is contained in:
parent
b8b05141ee
commit
f082df7f33
11 changed files with 69 additions and 44 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)."
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue