Răsfoiți Sursa

Merge pull request #8469 from ThomasWaldmann/special-tags

implement special tags, @PROT for protecting archives
TW 7 luni în urmă
părinte
comite
e376b7f2fa

+ 1 - 0
src/borg/archiver/delete_cmd.py

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

+ 1 - 0
src/borg/archiver/prune_cmd.py

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

+ 3 - 2
src/borg/archiver/recreate_cmd.py

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

+ 27 - 2
src/borg/archiver/tag_cmd.py

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

+ 4 - 0
src/borg/constants.py

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

+ 16 - 0
src/borg/testsuite/archiver/delete_cmd_test.py

@@ -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 - 0
src/borg/testsuite/archiver/prune_cmd_test.py

@@ -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 - 0
src/borg/testsuite/archiver/recreate_cmd_test.py

@@ -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 - 0
src/borg/testsuite/archiver/tag_cmd_test.py

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