Merge pull request #8469 from ThomasWaldmann/special-tags

implement special tags, @PROT for protecting archives
This commit is contained in:
TW 2024-10-10 13:37:39 +02:00 committed by GitHub
commit e376b7f2fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 114 additions and 4 deletions

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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(

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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")