mirror of
https://github.com/borgbackup/borg.git
synced 2026-06-10 17:32:13 -04:00
Merge pull request #8469 from ThomasWaldmann/special-tags
implement special tags, @PROT for protecting archives
This commit is contained in:
commit
e376b7f2fa
9 changed files with 114 additions and 4 deletions
|
|
@ -22,6 +22,7 @@ class DeleteMixIn:
|
|||
archive_infos = [manifest.archives.get_one([args.name])]
|
||||
else:
|
||||
archive_infos = manifest.archives.list_considering(args)
|
||||
archive_infos = [ai for ai in archive_infos if "@PROT" not in ai.tags]
|
||||
count = len(archive_infos)
|
||||
if count == 0:
|
||||
return
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@ class PruneMixIn:
|
|||
|
||||
match = args.name if args.name else args.match_archives
|
||||
archives = manifest.archives.list(match=match, sort_by=["ts"], reverse=True)
|
||||
archives = [ai for ai in archives if "@PROT" not in ai.tags]
|
||||
|
||||
keep = []
|
||||
# collect the rule responsible for the keeping of each archive in this dict
|
||||
|
|
|
|||
|
|
@ -37,8 +37,9 @@ class RecreateMixIn:
|
|||
dry_run=args.dry_run,
|
||||
timestamp=args.timestamp,
|
||||
)
|
||||
|
||||
for archive_info in manifest.archives.list_considering(args):
|
||||
archive_infos = manifest.archives.list_considering(args)
|
||||
archive_infos = [ai for ai in archive_infos if "@PROT" not in ai.tags]
|
||||
for archive_info in archive_infos:
|
||||
if recreater.is_temporary_archive(archive_info.name):
|
||||
continue
|
||||
name, hex_id = archive_info.name, bin_to_hex(archive_info.id)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import argparse
|
|||
from ._common import with_repository, define_archive_filters_group
|
||||
from ..archive import Archive
|
||||
from ..constants import * # NOQA
|
||||
from ..helpers import bin_to_hex, archivename_validator, tag_validator
|
||||
from ..helpers import bin_to_hex, archivename_validator, tag_validator, Error
|
||||
from ..manifest import Manifest
|
||||
|
||||
from ..logger import create_logger
|
||||
|
|
@ -25,10 +25,26 @@ class TagMixIn:
|
|||
else:
|
||||
archive_infos = manifest.archives.list_considering(args)
|
||||
|
||||
def check_special(tags):
|
||||
if tags:
|
||||
special = {tag for tag in tags_set(tags) if tag.startswith("@")}
|
||||
if not special.issubset(SPECIAL_TAGS):
|
||||
raise Error("unknown special tags given.")
|
||||
|
||||
check_special(args.set_tags)
|
||||
check_special(args.add_tags)
|
||||
check_special(args.remove_tags)
|
||||
|
||||
for archive_info in archive_infos:
|
||||
archive = Archive(manifest, archive_info.id, cache=cache)
|
||||
if args.set_tags:
|
||||
archive.tags = tags_set(args.set_tags)
|
||||
# avoid that --set (accidentally) erases existing special tags,
|
||||
# but allow --set if the existing special tags are also given.
|
||||
new_tags = tags_set(args.set_tags)
|
||||
existing_special = {tag for tag in archive.tags if tag.startswith("@")}
|
||||
clobber = not existing_special.issubset(new_tags)
|
||||
if not clobber:
|
||||
archive.tags = new_tags
|
||||
if args.add_tags:
|
||||
archive.tags |= tags_set(args.add_tags)
|
||||
if args.remove_tags:
|
||||
|
|
@ -53,6 +69,15 @@ class TagMixIn:
|
|||
|
||||
You can set the tags to a specific set of tags or you can add or remove
|
||||
tags from the current set of tags.
|
||||
|
||||
User defined tags must not start with `@` because such tags are considered
|
||||
special and users are only allowed to use known special tags:
|
||||
|
||||
``@PROT``: protects archives against archive deletion or pruning.
|
||||
|
||||
Pre-existing special tags can not be removed via ``--set``. You can still use
|
||||
``--set``, but you must give pre-existing special tags also (so they won't be
|
||||
removed).
|
||||
"""
|
||||
)
|
||||
subparser = subparsers.add_parser(
|
||||
|
|
|
|||
|
|
@ -124,6 +124,10 @@ TIME_DIFFERS2_NS = 3000000000
|
|||
# tar related
|
||||
SCHILY_XATTR = "SCHILY.xattr." # xattr key prefix in tar PAX headers
|
||||
|
||||
# special tags
|
||||
# @PROT protects archives against accidential deletion or modification by delete, prune or recreate.
|
||||
SPECIAL_TAGS = frozenset(["@PROT"])
|
||||
|
||||
# return codes returned by borg command
|
||||
EXIT_SUCCESS = 0 # everything done, no problems
|
||||
EXIT_WARNING = 1 # reached normal end of operation, but there were issues (generic warning)
|
||||
|
|
|
|||
|
|
@ -32,3 +32,19 @@ def test_delete_multiple(archivers, request):
|
|||
cmd(archiver, "delete", "-a", "test1")
|
||||
cmd(archiver, "delete", "-a", "test2")
|
||||
assert not cmd(archiver, "repo-list")
|
||||
|
||||
|
||||
def test_delete_ignore_protected(archivers, request):
|
||||
archiver = request.getfixturevalue(archivers)
|
||||
create_regular_file(archiver.input_path, "file1", size=1024 * 80)
|
||||
cmd(archiver, "repo-create", RK_ENCRYPTION)
|
||||
cmd(archiver, "create", "test1", "input")
|
||||
cmd(archiver, "tag", "--add=@PROT", "test1")
|
||||
cmd(archiver, "create", "test2", "input")
|
||||
cmd(archiver, "delete", "-a", "test1")
|
||||
cmd(archiver, "delete", "-a", "test2")
|
||||
cmd(archiver, "delete", "-a", "sh:test*")
|
||||
output = cmd(archiver, "repo-list")
|
||||
assert "@PROT" in output
|
||||
assert "test1" in output
|
||||
assert "test2" not in output
|
||||
|
|
|
|||
|
|
@ -241,3 +241,19 @@ def test_prune_repository_glob(archivers, request):
|
|||
assert "2015-08-12-20:00-foo" in output
|
||||
assert "2015-08-12-10:00-bar" in output
|
||||
assert "2015-08-12-20:00-bar" in output
|
||||
|
||||
|
||||
def test_prune_ignore_protected(archivers, request):
|
||||
archiver = request.getfixturevalue(archivers)
|
||||
cmd(archiver, "repo-create", RK_ENCRYPTION)
|
||||
cmd(archiver, "create", "archive1", archiver.input_path)
|
||||
cmd(archiver, "tag", "--set=@PROT", "archive1") # do not delete archive1!
|
||||
cmd(archiver, "create", "archive2", archiver.input_path)
|
||||
cmd(archiver, "create", "archive3", archiver.input_path)
|
||||
output = cmd(archiver, "prune", "--list", "--keep-last=1", "--match-archives=sh:archive*")
|
||||
assert "archive1" not in output # @PROT archives are completely ignored.
|
||||
assert re.search(r"Keeping archive \(rule: secondly #1\):\s+archive3", output)
|
||||
assert re.search(r"Pruning archive \(.*?\):\s+archive2", output)
|
||||
output = cmd(archiver, "repo-list")
|
||||
assert "archive1" in output # @PROT protected archive1 from deletion
|
||||
assert "archive3" in output # last one
|
||||
|
|
|
|||
|
|
@ -274,3 +274,18 @@ def test_comment(archivers, request):
|
|||
assert "Comment: modified comment" in cmd(archiver, "info", "-a", "test2")
|
||||
assert "Comment: " + os.linesep in cmd(archiver, "info", "-a", "test3")
|
||||
assert "Comment: preserved comment" in cmd(archiver, "info", "-a", "test4")
|
||||
|
||||
|
||||
def test_recreate_ignore_protected(archivers, request):
|
||||
archiver = request.getfixturevalue(archivers)
|
||||
create_test_files(archiver.input_path)
|
||||
create_regular_file(archiver.input_path, "file1", size=1024)
|
||||
create_regular_file(archiver.input_path, "file2", size=1024)
|
||||
cmd(archiver, "repo-create", RK_ENCRYPTION)
|
||||
cmd(archiver, "create", "archive", "input")
|
||||
cmd(archiver, "tag", "--add=@PROT", "archive")
|
||||
cmd(archiver, "recreate", "archive", "-e", "input") # this would normally remove all from archive
|
||||
listing = cmd(archiver, "list", "archive", "--short")
|
||||
# archive was protected, so recreate ignored it:
|
||||
assert "file1" in listing
|
||||
assert "file2" in listing
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import pytest
|
||||
|
||||
from ...constants import * # NOQA
|
||||
from . import cmd, generate_archiver_tests, RK_ENCRYPTION
|
||||
from ...helpers import Error
|
||||
|
||||
pytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds="local") # NOQA
|
||||
|
||||
|
|
@ -30,3 +33,31 @@ def test_tag_add_remove(archivers, request):
|
|||
assert "tags: bb." in output
|
||||
output = cmd(archiver, "tag", "-a", "archive", "--remove", "bb")
|
||||
assert "tags: ." in output
|
||||
|
||||
|
||||
def test_tag_set_noclobber_special(archivers, request):
|
||||
archiver = request.getfixturevalue(archivers)
|
||||
cmd(archiver, "repo-create", RK_ENCRYPTION)
|
||||
cmd(archiver, "create", "archive", archiver.input_path)
|
||||
output = cmd(archiver, "tag", "-a", "archive", "--set", "@PROT")
|
||||
assert "tags: @PROT." in output
|
||||
# archive now has a special tag.
|
||||
# it must not be possible to accidentally erase such special tags by using --set:
|
||||
output = cmd(archiver, "tag", "-a", "archive", "--set", "clobber")
|
||||
assert "tags: @PROT." in output
|
||||
# it is possible though to use --set if the existing special tags are also given:
|
||||
output = cmd(archiver, "tag", "-a", "archive", "--set", "noclobber", "--set", "@PROT")
|
||||
assert "tags: @PROT,noclobber." in output
|
||||
|
||||
|
||||
def test_tag_only_known_special(archivers, request):
|
||||
archiver = request.getfixturevalue(archivers)
|
||||
cmd(archiver, "repo-create", RK_ENCRYPTION)
|
||||
cmd(archiver, "create", "archive", archiver.input_path)
|
||||
# user can't set / add / remove unknown special tags
|
||||
with pytest.raises(Error):
|
||||
cmd(archiver, "tag", "-a", "archive", "--set", "@UNKNOWN")
|
||||
with pytest.raises(Error):
|
||||
cmd(archiver, "tag", "-a", "archive", "--add", "@UNKNOWN")
|
||||
with pytest.raises(Error):
|
||||
cmd(archiver, "tag", "-a", "archive", "--remove", "@UNKNOWN")
|
||||
|
|
|
|||
Loading…
Reference in a new issue