Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement special tags, @PROT for protecting archives #8469

Merged
merged 3 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/borg/archiver/delete_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def do_delete(self, args, repository):
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
Expand Down
1 change: 1 addition & 0 deletions src/borg/archiver/prune_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ def do_prune(self, args, repository, manifest):

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
Expand Down
5 changes: 3 additions & 2 deletions src/borg/archiver/recreate_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ def do_recreate(self, args, repository, manifest, cache):
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)
Expand Down
29 changes: 27 additions & 2 deletions src/borg/archiver/tag_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
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
Expand All @@ -25,10 +25,26 @@ def tags_set(tags):
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:
Expand All @@ -53,6 +69,15 @@ def build_parser_tag(self, subparsers, common_parser, mid_common_parser):

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(
Expand Down
4 changes: 4 additions & 0 deletions src/borg/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@
# 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)
Expand Down
16 changes: 16 additions & 0 deletions src/borg/testsuite/archiver/delete_cmd_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
16 changes: 16 additions & 0 deletions src/borg/testsuite/archiver/prune_cmd_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
15 changes: 15 additions & 0 deletions src/borg/testsuite/archiver/recreate_cmd_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
31 changes: 31 additions & 0 deletions src/borg/testsuite/archiver/tag_cmd_test.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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