diff --git a/docs/man/borg-tag.1 b/docs/man/borg-tag.1 new file mode 100644 index 000000000..3f264a8db --- /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 1cceadb5c..a530732e8 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 000000000..b08162688 --- /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 000000000..3890f01dd --- /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 b3f0b308c..add009bea 100644 --- a/src/borg/archiver/__init__.py +++ b/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) diff --git a/src/borg/archiver/tag_cmd.py b/src/borg/archiver/tag_cmd.py new file mode 100644 index 000000000..de9b44499 --- /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 23833dd52..3d1bc74a1 100644 --- a/src/borg/helpers/__init__.py +++ b/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 diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index 171e1462d..b43397f33 100644 --- a/src/borg/helpers/parseformat.py +++ b/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): diff --git a/src/borg/testsuite/archiver/tag_cmd.py b/src/borg/testsuite/archiver/tag_cmd.py new file mode 100644 index 000000000..a5e556f4c --- /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