Prechádzať zdrojové kódy

tag: set, add, remove tags

Thomas Waldmann 8 mesiacov pred
rodič
commit
e274860983

+ 98 - 0
docs/man/borg-tag.1

@@ -0,0 +1,98 @@
+.\" Man page generated from reStructuredText.
+.
+.
+.nr rst2man-indent-level 0
+.
+.de1 rstReportMargin
+\\$1 \\n[an-margin]
+level \\n[rst2man-indent-level]
+level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
+-
+\\n[rst2man-indent0]
+\\n[rst2man-indent1]
+\\n[rst2man-indent2]
+..
+.de1 INDENT
+.\" .rstReportMargin pre:
+. RS \\$1
+. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin]
+. nr rst2man-indent-level +1
+.\" .rstReportMargin post:
+..
+.de UNINDENT
+. RE
+.\" indent \\n[an-margin]
+.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]]
+.nr rst2man-indent-level -1
+.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
+.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
+..
+.TH "BORG-TAG" 1 "2024-10-02" "" "borg backup tool"
+.SH NAME
+borg-tag \- Manage tags
+.SH SYNOPSIS
+.sp
+borg [common options] tag [options] [NAME]
+.SH DESCRIPTION
+.sp
+Manage archive tags.
+.sp
+Borg archives can have a set of tags which can be used for matching archives.
+.sp
+You can set the tags to a specific set of tags or you can add or remove
+tags from the current set of tags.
+.SH OPTIONS
+.sp
+See \fIborg\-common(1)\fP for common options of Borg commands.
+.SS arguments
+.INDENT 0.0
+.TP
+.B NAME
+specify the archive name
+.UNINDENT
+.SS optional arguments
+.INDENT 0.0
+.TP
+.BI \-\-set \ TAG
+set tags (can be given multiple times)
+.TP
+.BI \-\-add \ TAG
+add tags (can be given multiple times)
+.TP
+.BI \-\-remove \ TAG
+remove tags (can be given multiple times)
+.UNINDENT
+.SS Archive filters
+.INDENT 0.0
+.TP
+.BI \-a \ PATTERN\fR,\fB \ \-\-match\-archives \ PATTERN
+only consider archives matching all patterns. see \(dqborg help match\-archives\(dq.
+.TP
+.BI \-\-sort\-by \ KEYS
+Comma\-separated list of sorting keys; valid keys are: timestamp, archive, name, id, tags, host, user; default is: timestamp
+.TP
+.BI \-\-first \ N
+consider first N archives after other filters were applied
+.TP
+.BI \-\-last \ N
+consider last N archives after other filters were applied
+.TP
+.BI \-\-oldest \ TIMESPAN
+consider archives between the oldest archive\(aqs timestamp and (oldest + TIMESPAN), e.g. 7d or 12m.
+.TP
+.BI \-\-newest \ TIMESPAN
+consider archives between the newest archive\(aqs timestamp and (newest \- TIMESPAN), e.g. 7d or 12m.
+.TP
+.BI \-\-older \ TIMESPAN
+consider archives older than (now \- TIMESPAN), e.g. 7d or 12m.
+.TP
+.BI \-\-newer \ TIMESPAN
+consider archives newer than (now \- TIMESPAN), e.g. 7d or 12m.
+.UNINDENT
+.SH SEE ALSO
+.sp
+\fIborg\-common(1)\fP
+.SH AUTHOR
+The Borg Collective
+.\" Generated by docutils manpage writer.
+.

+ 2 - 1
docs/usage.rst

@@ -51,8 +51,9 @@ Usage
    usage/create
    usage/extract
    usage/check
-   usage/rename
    usage/list
+   usage/tag
+   usage/rename
    usage/diff
    usage/delete
    usage/prune

+ 1 - 0
docs/usage/tag.rst

@@ -0,0 +1 @@
+.. include:: tag.rst.inc

+ 93 - 0
docs/usage/tag.rst.inc

@@ -0,0 +1,93 @@
+.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!
+
+.. _borg_tag:
+
+borg tag
+--------
+.. code-block:: none
+
+    borg [common options] tag [options] [NAME]
+
+.. only:: html
+
+    .. class:: borg-options-table
+
+    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+
+    | **positional arguments**                                                                                                                                                                                                                                 |
+    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+
+    |                                                                             | ``NAME``                                     | specify the archive name                                                                                                    |
+    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+
+    | **optional arguments**                                                                                                                                                                                                                                   |
+    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+
+    |                                                                             | ``--set TAG``                                | set tags (can be given multiple times)                                                                                      |
+    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+
+    |                                                                             | ``--add TAG``                                | add tags (can be given multiple times)                                                                                      |
+    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+
+    |                                                                             | ``--remove TAG``                             | remove tags (can be given multiple times)                                                                                   |
+    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+
+    | .. class:: borg-common-opt-ref                                                                                                                                                                                                                           |
+    |                                                                                                                                                                                                                                                          |
+    | :ref:`common_options`                                                                                                                                                                                                                                    |
+    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+
+    | **Archive filters** — Archive filters can be applied to repository targets.                                                                                                                                                                              |
+    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+
+    |                                                                             | ``-a PATTERN``, ``--match-archives PATTERN`` | only consider archives matching all patterns. see "borg help match-archives".                                               |
+    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+
+    |                                                                             | ``--sort-by KEYS``                           | Comma-separated list of sorting keys; valid keys are: timestamp, archive, name, id, tags, host, user; default is: timestamp |
+    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+
+    |                                                                             | ``--first N``                                | consider first N archives after other filters were applied                                                                  |
+    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+
+    |                                                                             | ``--last N``                                 | consider last N archives after other filters were applied                                                                   |
+    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+
+    |                                                                             | ``--oldest TIMESPAN``                        | consider archives between the oldest archive's timestamp and (oldest + TIMESPAN), e.g. 7d or 12m.                           |
+    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+
+    |                                                                             | ``--newest TIMESPAN``                        | consider archives between the newest archive's timestamp and (newest - TIMESPAN), e.g. 7d or 12m.                           |
+    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+
+    |                                                                             | ``--older TIMESPAN``                         | consider archives older than (now - TIMESPAN), e.g. 7d or 12m.                                                              |
+    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+
+    |                                                                             | ``--newer TIMESPAN``                         | consider archives newer than (now - TIMESPAN), e.g. 7d or 12m.                                                              |
+    +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+
+
+    .. raw:: html
+
+        <script type='text/javascript'>
+        $(document).ready(function () {
+            $('.borg-options-table colgroup').remove();
+        })
+        </script>
+
+.. only:: latex
+
+    NAME
+        specify the archive name
+
+
+    optional arguments
+        --set TAG     set tags (can be given multiple times)
+        --add TAG     add tags (can be given multiple times)
+        --remove TAG    remove tags (can be given multiple times)
+
+
+    :ref:`common_options`
+        |
+
+    Archive filters
+        -a PATTERN, --match-archives PATTERN     only consider archives matching all patterns. see "borg help match-archives".
+        --sort-by KEYS                           Comma-separated list of sorting keys; valid keys are: timestamp, archive, name, id, tags, host, user; default is: timestamp
+        --first N                                consider first N archives after other filters were applied
+        --last N                                 consider last N archives after other filters were applied
+        --oldest TIMESPAN                        consider archives between the oldest archive's timestamp and (oldest + TIMESPAN), e.g. 7d or 12m.
+        --newest TIMESPAN                        consider archives between the newest archive's timestamp and (newest - TIMESPAN), e.g. 7d or 12m.
+        --older TIMESPAN                         consider archives older than (now - TIMESPAN), e.g. 7d or 12m.
+        --newer TIMESPAN                         consider archives newer than (now - TIMESPAN), e.g. 7d or 12m.
+
+
+Description
+~~~~~~~~~~~
+
+Manage archive tags.
+
+Borg archives can have a set of tags which can be used for matching archives.
+
+You can set the tags to a specific set of tags or you can add or remove
+tags from the current set of tags.

+ 3 - 0
src/borg/archiver/__init__.py

@@ -89,6 +89,7 @@ from .repo_delete_cmd import RepoDeleteMixIn
 from .repo_list_cmd import RepoListMixIn
 from .repo_space_cmd import RepoSpaceMixIn
 from .serve_cmd import ServeMixIn
+from .tag_cmd import TagMixIn
 from .tar_cmds import TarMixIn
 from .transfer_cmd import TransferMixIn
 from .version_cmd import VersionMixIn
@@ -120,6 +121,7 @@ class Archiver(
     RepoListMixIn,
     RepoSpaceMixIn,
     ServeMixIn,
+    TagMixIn,
     TarMixIn,
     TransferMixIn,
     VersionMixIn,
@@ -359,6 +361,7 @@ class Archiver(
         self.build_parser_rename(subparsers, common_parser, mid_common_parser)
         self.build_parser_repo_space(subparsers, common_parser, mid_common_parser)
         self.build_parser_serve(subparsers, common_parser, mid_common_parser)
+        self.build_parser_tag(subparsers, common_parser, mid_common_parser)
         self.build_parser_tar(subparsers, common_parser, mid_common_parser)
         self.build_parser_transfer(subparsers, common_parser, mid_common_parser)
         self.build_parser_version(subparsers, common_parser, mid_common_parser)

+ 95 - 0
src/borg/archiver/tag_cmd.py

@@ -0,0 +1,95 @@
+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 ..manifest import Manifest
+
+from ..logger import create_logger
+
+logger = create_logger()
+
+
+class TagMixIn:
+    @with_repository(cache=True, compatibility=(Manifest.Operation.WRITE,))
+    def do_tag(self, args, repository, manifest, cache):
+        """Manage tags"""
+
+        def tags_set(tags):
+            """return a set of tags, removing empty tags"""
+            return set(tag for tag in tags if tag)
+
+        if args.name:
+            archive_infos = [manifest.archives.get_one([args.name])]
+        else:
+            archive_infos = manifest.archives.list_considering(args)
+
+        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)
+            if args.add_tags:
+                archive.tags |= tags_set(args.add_tags)
+            if args.remove_tags:
+                archive.tags -= tags_set(args.remove_tags)
+            old_id = archive.id
+            archive.set_meta("tags", list(sorted(archive.tags)))
+            if old_id != archive.id:
+                manifest.archives.delete_by_id(old_id)
+            print(
+                f"id: {bin_to_hex(old_id):.8} -> {bin_to_hex(archive.id):.8}, "
+                f"tags: {','.join(sorted(archive.tags))}."
+            )
+
+    def build_parser_tag(self, subparsers, common_parser, mid_common_parser):
+        from ._common import process_epilog
+
+        tag_epilog = process_epilog(
+            """
+            Manage archive tags.
+
+            Borg archives can have a set of tags which can be used for matching archives.
+
+            You can set the tags to a specific set of tags or you can add or remove
+            tags from the current set of tags.
+            """
+        )
+        subparser = subparsers.add_parser(
+            "tag",
+            parents=[common_parser],
+            add_help=False,
+            description=self.do_tag.__doc__,
+            epilog=tag_epilog,
+            formatter_class=argparse.RawDescriptionHelpFormatter,
+            help="tag archives",
+        )
+        subparser.set_defaults(func=self.do_tag)
+        subparser.add_argument(
+            "--set",
+            dest="set_tags",
+            metavar="TAG",
+            type=tag_validator,
+            action="append",
+            help="set tags (can be given multiple times)",
+        )
+        subparser.add_argument(
+            "--add",
+            dest="add_tags",
+            metavar="TAG",
+            type=tag_validator,
+            action="append",
+            help="add tags (can be given multiple times)",
+        )
+        subparser.add_argument(
+            "--remove",
+            dest="remove_tags",
+            metavar="TAG",
+            type=tag_validator,
+            action="append",
+            help="remove tags (can be given multiple times)",
+        )
+        define_archive_filters_group(subparser)
+        subparser.add_argument(
+            "name", metavar="NAME", nargs="?", type=archivename_validator, help="specify the archive name"
+        )

+ 1 - 1
src/borg/helpers/__init__.py

@@ -34,7 +34,7 @@ from .parseformat import format_file_size, parse_file_size, FileSize, parse_stor
 from .parseformat import sizeof_fmt, sizeof_fmt_iec, sizeof_fmt_decimal, Location, text_validator
 from .parseformat import format_line, replace_placeholders, PlaceholderError, relative_time_marker_validator
 from .parseformat import format_archive, parse_stringified_list, clean_lines
-from .parseformat import location_validator, archivename_validator, comment_validator
+from .parseformat import location_validator, archivename_validator, comment_validator, tag_validator
 from .parseformat import BaseFormatter, ArchiveFormatter, ItemFormatter, DiffFormatter, file_status
 from .parseformat import swidth_slice, ellipsis_truncate
 from .parseformat import BorgJsonEncoder, basic_json_data, json_print, json_dump, prepare_dump_dict

+ 1 - 0
src/borg/helpers/parseformat.py

@@ -685,6 +685,7 @@ def text_validator(*, name, max_length, min_length=0, invalid_ctrl_chars="\0", i
 
 
 comment_validator = text_validator(name="comment", max_length=10000)
+tag_validator = text_validator(name="tag", min_length=0, max_length=10, invalid_chars=" ,$")
 
 
 def archivename_validator(text):

+ 32 - 0
src/borg/testsuite/archiver/tag_cmd.py

@@ -0,0 +1,32 @@
+from ...constants import *  # NOQA
+from . import cmd, generate_archiver_tests, RK_ENCRYPTION
+
+pytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds="local")  # NOQA
+
+
+def test_tag_set(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", "aa")
+    assert "tags: aa." in output
+    output = cmd(archiver, "tag", "-a", "archive", "--set", "bb")
+    assert "tags: bb." in output
+    output = cmd(archiver, "tag", "-a", "archive", "--set", "bb", "--set", "aa")
+    assert "tags: aa,bb." in output  # sorted!
+    output = cmd(archiver, "tag", "-a", "archive", "--set", "")
+    assert "tags: ." in output  # no tags!
+
+
+def test_tag_add_remove(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", "--add", "aa")
+    assert "tags: aa." in output
+    output = cmd(archiver, "tag", "-a", "archive", "--add", "bb")
+    assert "tags: aa,bb." in output
+    output = cmd(archiver, "tag", "-a", "archive", "--remove", "aa")
+    assert "tags: bb." in output
+    output = cmd(archiver, "tag", "-a", "archive", "--remove", "bb")
+    assert "tags: ." in output