From e274860983c10de4efefba75d5df9f8b0c54866a Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 2 Oct 2024 23:28:13 +0200 Subject: [PATCH 1/2] tag: set, add, remove tags --- docs/man/borg-tag.1 | 98 ++++++++++++++++++++++++++ docs/usage.rst | 3 +- docs/usage/tag.rst | 1 + docs/usage/tag.rst.inc | 93 ++++++++++++++++++++++++ src/borg/archiver/__init__.py | 3 + src/borg/archiver/tag_cmd.py | 95 +++++++++++++++++++++++++ src/borg/helpers/__init__.py | 2 +- src/borg/helpers/parseformat.py | 1 + src/borg/testsuite/archiver/tag_cmd.py | 32 +++++++++ 9 files changed, 326 insertions(+), 2 deletions(-) create mode 100644 docs/man/borg-tag.1 create mode 100644 docs/usage/tag.rst create mode 100644 docs/usage/tag.rst.inc create mode 100644 src/borg/archiver/tag_cmd.py create mode 100644 src/borg/testsuite/archiver/tag_cmd.py diff --git a/docs/man/borg-tag.1 b/docs/man/borg-tag.1 new file mode 100644 index 0000000000..3f264a8db9 --- /dev/null +++ b/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. +. diff --git a/docs/usage.rst b/docs/usage.rst index 1cceadb5c1..a530732e8f 100644 --- a/docs/usage.rst +++ b/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 diff --git a/docs/usage/tag.rst b/docs/usage/tag.rst new file mode 100644 index 0000000000..b081626880 --- /dev/null +++ b/docs/usage/tag.rst @@ -0,0 +1 @@ +.. include:: tag.rst.inc diff --git a/docs/usage/tag.rst.inc b/docs/usage/tag.rst.inc new file mode 100644 index 0000000000..3890f01dda --- /dev/null +++ b/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 + + + +.. 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. \ No newline at end of file diff --git a/src/borg/archiver/__init__.py b/src/borg/archiver/__init__.py index b3f0b308c2..add009bea0 100644 --- a/src/borg/archiver/__init__.py +++ b/src/borg/archiver/__init__.py @@ -89,6 +89,7 @@ def get_func(args): 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 @@ def build_parser(self): 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) diff --git a/src/borg/archiver/tag_cmd.py b/src/borg/archiver/tag_cmd.py new file mode 100644 index 0000000000..de9b444992 --- /dev/null +++ b/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" + ) diff --git a/src/borg/helpers/__init__.py b/src/borg/helpers/__init__.py index 23833dd52d..3d1bc74a14 100644 --- a/src/borg/helpers/__init__.py +++ b/src/borg/helpers/__init__.py @@ -34,7 +34,7 @@ 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 diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index 171e1462dc..b43397f33c 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -685,6 +685,7 @@ def validator(text): 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): diff --git a/src/borg/testsuite/archiver/tag_cmd.py b/src/borg/testsuite/archiver/tag_cmd.py new file mode 100644 index 0000000000..a5e556f4cb --- /dev/null +++ b/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 From ae0e794355fe7d036eca1063ac22e6b2a4706009 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 3 Oct 2024 00:15:04 +0200 Subject: [PATCH 2/2] repo-list: show tags --- src/borg/archiver/repo_list_cmd.py | 3 ++- src/borg/helpers/parseformat.py | 7 ++++++- src/borg/testsuite/archiver/repo_list_cmd.py | 2 +- src/borg/testsuite/archiver/transfer_cmd.py | 1 + 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/borg/archiver/repo_list_cmd.py b/src/borg/archiver/repo_list_cmd.py index 6214c2ae06..752b706f28 100644 --- a/src/borg/archiver/repo_list_cmd.py +++ b/src/borg/archiver/repo_list_cmd.py @@ -23,7 +23,8 @@ def do_repo_list(self, args, repository, manifest): format = "{id}{NL}" else: format = os.environ.get( - "BORG_RLIST_FORMAT", "{id:.8} {time} {archive:<15} {username:<10} {hostname:<10} {comment:.40}{NL}" + "BORG_RLIST_FORMAT", + "{id:.8} {time} {archive:<15} {tags:<10} {username:<10} {hostname:<10} {comment:.40}{NL}", ) formatter = ArchiveFormatter(format, repository, manifest, manifest.key, iec=args.iec) diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index b43397f33c..5bffa6fe3f 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -771,6 +771,7 @@ class ArchiveFormatter(BaseFormatter): "archive": "archive name", "name": 'alias of "archive"', "comment": "archive comment", + "tags": "archive tags", # *start* is the key used by borg-info for this timestamp, this makes the formats more compatible "start": "time (start) of creation of the archive", "time": 'alias of "start"', @@ -783,7 +784,7 @@ class ArchiveFormatter(BaseFormatter): "nfiles": "count of files in this archive", } KEY_GROUPS = ( - ("archive", "name", "comment", "id"), + ("archive", "name", "comment", "id", "tags"), ("start", "time", "end", "command_line"), ("hostname", "username"), ("size", "nfiles"), @@ -809,6 +810,7 @@ def __init__(self, format, repository, manifest, key, *, iec=False): "size": partial(self.get_meta, "size", 0), "nfiles": partial(self.get_meta, "nfiles", 0), "end": self.get_ts_end, + "tags": self.get_tags, } self.used_call_keys = set(self.call_keys) & self.format_keys @@ -854,6 +856,9 @@ def get_ts_end(self): def format_time(self, ts): return OutputTimestamp(ts) + def get_tags(self): + return ",".join(sorted(self.archive.tags)) + class ItemFormatter(BaseFormatter): # we provide the hash algos from python stdlib (except shake_*) and additionally xxh64. diff --git a/src/borg/testsuite/archiver/repo_list_cmd.py b/src/borg/testsuite/archiver/repo_list_cmd.py index fc3b030678..ffce5af007 100644 --- a/src/borg/testsuite/archiver/repo_list_cmd.py +++ b/src/borg/testsuite/archiver/repo_list_cmd.py @@ -29,7 +29,7 @@ def test_archives_format(archivers, request): archiver, "repo-list", "--format", - "{id:.8} {time} {archive:<15} {username:<10} {hostname:<10} {comment:.40}{NL}", + "{id:.8} {time} {archive:<15} {tags:<10} {username:<10} {hostname:<10} {comment:.40}{NL}", ) assert output_1 == output_2 output = cmd(archiver, "repo-list", "--short") diff --git a/src/borg/testsuite/archiver/transfer_cmd.py b/src/borg/testsuite/archiver/transfer_cmd.py index 84b9127e15..3ff2616f71 100644 --- a/src/borg/testsuite/archiver/transfer_cmd.py +++ b/src/borg/testsuite/archiver/transfer_cmd.py @@ -96,6 +96,7 @@ def convert_tz(local_naive, tzoffset, tzinfo): del got_archive["username"] # we didn't have this in the 1.x default format del got_archive["hostname"] # we didn't have this in the 1.x default format del got_archive["comment"] # we didn't have this in the 1.x default format + del got_archive["tags"] # we didn't have this in the 1.x default format del expected_archive["id"] del expected_archive["barchive"] # timestamps: