mirror of
https://github.com/borgbackup/borg.git
synced 2026-02-20 00:10:35 -05:00
Merge 2fcd54e891 into 045701558e
This commit is contained in:
commit
c2709491ef
38 changed files with 509 additions and 274 deletions
|
|
@ -37,6 +37,7 @@ dependencies = [
|
|||
"platformdirs >=3.0.0, <5.0.0; sys_platform == 'darwin'", # for macOS: breaking changes in 3.0.0.
|
||||
"platformdirs >=2.6.0, <5.0.0; sys_platform != 'darwin'", # for others: 2.6+ works consistently.
|
||||
"argon2-cffi",
|
||||
"jsonargparse>=4.27.0",
|
||||
"shtab>=1.8.0",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ class BuildUsage:
|
|||
is_subcommand = False
|
||||
choices = {}
|
||||
for action in parser._actions:
|
||||
if action.choices is not None and "SubParsersAction" in str(action.__class__):
|
||||
if action.choices is not None and "_ActionSubCommands" in str(action.__class__):
|
||||
is_subcommand = True
|
||||
for cmd, parser in action.choices.items():
|
||||
choices[prefix + cmd] = parser
|
||||
|
|
@ -323,7 +323,7 @@ class BuildMan:
|
|||
is_subcommand = False
|
||||
choices = {}
|
||||
for action in parser._actions:
|
||||
if action.choices is not None and "SubParsersAction" in str(action.__class__):
|
||||
if action.choices is not None and "_ActionSubCommands" in str(action.__class__):
|
||||
is_subcommand = True
|
||||
for cmd, parser in action.choices.items():
|
||||
choices[prefix + cmd] = parser
|
||||
|
|
@ -349,7 +349,7 @@ class BuildMan:
|
|||
|
||||
self.write_heading(write, "SYNOPSIS")
|
||||
if is_intermediary:
|
||||
subparsers = [action for action in parser._actions if "SubParsersAction" in str(action.__class__)][0]
|
||||
subparsers = [action for action in parser._actions if "_ActionSubCommands" in str(action.__class__)][0]
|
||||
for subcommand in subparsers.choices:
|
||||
write("| borg", "[common options]", command, subcommand, "...")
|
||||
self.see_also.setdefault(command, []).append(f"{command}-{subcommand}")
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ try:
|
|||
from ..helpers import ErrorIgnoringTextIOWrapper
|
||||
from ..helpers import msgpack
|
||||
from ..helpers import sig_int
|
||||
from ..helpers.jap_wrapper import ArgumentParser, flatten_namespace
|
||||
from ..remote import RemoteRepository
|
||||
from ..selftest import selftest
|
||||
except BaseException:
|
||||
|
|
@ -63,18 +64,6 @@ STATS_HEADER = " Original size Deduplicated size"
|
|||
PURE_PYTHON_MSGPACK_WARNING = "Using a pure-python msgpack! This will result in lower performance."
|
||||
|
||||
|
||||
def get_func(args):
|
||||
# This works around https://bugs.python.org/issue9351
|
||||
# func is used at the leaf parsers of the argparse parser tree,
|
||||
# fallback_func at next level towards the root,
|
||||
# fallback2_func at the 2nd next level (which is root in our case).
|
||||
for name in "func", "fallback_func", "fallback2_func":
|
||||
func = getattr(args, name, None)
|
||||
if func is not None:
|
||||
return func
|
||||
raise Exception("expected func attributes not found")
|
||||
|
||||
|
||||
from .analyze_cmd import AnalyzeMixIn
|
||||
from .benchmark_cmd import BenchmarkMixIn
|
||||
from .check_cmd import CheckMixIn
|
||||
|
|
@ -328,9 +317,7 @@ class Archiver(
|
|||
def build_parser(self):
|
||||
from ._common import define_common_options
|
||||
|
||||
parser = argparse.ArgumentParser(prog=self.prog, description="Borg - Deduplicated Backups", add_help=False)
|
||||
# paths and patterns must have an empty list as default everywhere
|
||||
parser.set_defaults(fallback2_func=functools.partial(self.do_maincommand_help, parser), paths=[], patterns=[])
|
||||
parser = ArgumentParser(prog=self.prog, description="Borg - Deduplicated Backups", add_help=False)
|
||||
parser.common_options = self.CommonOptions(
|
||||
define_common_options, suffix_precedence=("_maincommand", "_midcommand", "_subcommand")
|
||||
)
|
||||
|
|
@ -340,32 +327,29 @@ class Archiver(
|
|||
parser.add_argument("--cockpit", dest="cockpit", action="store_true", help="Start the Borg TUI")
|
||||
parser.common_options.add_common_group(parser, "_maincommand", provide_defaults=True)
|
||||
|
||||
common_parser = argparse.ArgumentParser(add_help=False, prog=self.prog)
|
||||
common_parser.set_defaults(paths=[], patterns=[])
|
||||
common_parser = ArgumentParser(add_help=False, prog=self.prog)
|
||||
parser.common_options.add_common_group(common_parser, "_subcommand")
|
||||
|
||||
mid_common_parser = argparse.ArgumentParser(add_help=False, prog=self.prog)
|
||||
mid_common_parser.set_defaults(paths=[], patterns=[])
|
||||
mid_common_parser = ArgumentParser(add_help=False, prog=self.prog)
|
||||
parser.common_options.add_common_group(mid_common_parser, "_midcommand")
|
||||
|
||||
if parser.prog == "borgfs":
|
||||
return self.build_parser_borgfs(parser)
|
||||
|
||||
subparsers = parser.add_subparsers(title="required arguments", metavar="<command>")
|
||||
subparsers = parser.add_subcommands(required=False)
|
||||
|
||||
# Phase 1: All level-1 subcommands (ALL must be added before any level-2).
|
||||
# Non-nested commands:
|
||||
self.build_parser_analyze(subparsers, common_parser, mid_common_parser)
|
||||
self.build_parser_benchmarks(subparsers, common_parser, mid_common_parser)
|
||||
self.build_parser_check(subparsers, common_parser, mid_common_parser)
|
||||
self.build_parser_compact(subparsers, common_parser, mid_common_parser)
|
||||
self.build_parser_completion(subparsers, common_parser, mid_common_parser)
|
||||
self.build_parser_create(subparsers, common_parser, mid_common_parser)
|
||||
self.build_parser_debug(subparsers, common_parser, mid_common_parser)
|
||||
self.build_parser_delete(subparsers, common_parser, mid_common_parser)
|
||||
self.build_parser_diff(subparsers, common_parser, mid_common_parser)
|
||||
self.build_parser_extract(subparsers, common_parser, mid_common_parser)
|
||||
self.build_parser_help(subparsers, common_parser, mid_common_parser, parser)
|
||||
self.build_parser_info(subparsers, common_parser, mid_common_parser)
|
||||
self.build_parser_keys(subparsers, common_parser, mid_common_parser)
|
||||
self.build_parser_list(subparsers, common_parser, mid_common_parser)
|
||||
self.build_parser_locks(subparsers, common_parser, mid_common_parser)
|
||||
self.build_parser_mount_umount(subparsers, common_parser, mid_common_parser)
|
||||
|
|
@ -384,12 +368,68 @@ class Archiver(
|
|||
self.build_parser_transfer(subparsers, common_parser, mid_common_parser)
|
||||
self.build_parser_undelete(subparsers, common_parser, mid_common_parser)
|
||||
self.build_parser_version(subparsers, common_parser, mid_common_parser)
|
||||
# Nested commands: add level-1 container parsers only
|
||||
benchmark_parser = self.build_parser_benchmarks_l1(subparsers, mid_common_parser)
|
||||
debug_parser = self.build_parser_debug_l1(subparsers, mid_common_parser)
|
||||
key_parser = self.build_parser_keys_l1(subparsers, mid_common_parser)
|
||||
|
||||
# Phase 2: All level-2 subcommands (must be after ALL level-1 are added).
|
||||
self.build_parser_benchmarks_l2(benchmark_parser, common_parser)
|
||||
self.build_parser_debug_l2(debug_parser, common_parser)
|
||||
self.build_parser_keys_l2(key_parser, common_parser)
|
||||
|
||||
# Build the commands dict for help and completion
|
||||
self._commands = self._build_commands_dict(subparsers)
|
||||
|
||||
return parser
|
||||
|
||||
def _build_commands_dict(self, subparsers):
|
||||
"""Build a dict mapping command names to their parsers for help/completion."""
|
||||
commands = {}
|
||||
# subparsers is an _ActionSubCommands instance with a .choices dict
|
||||
for name, parser in subparsers.choices.items():
|
||||
commands[name] = parser
|
||||
# For nested subcommands (key, debug, benchmark), check _subcommands_action
|
||||
nested_action = getattr(parser, "_subcommands_action", None)
|
||||
if nested_action is not None and hasattr(nested_action, "choices"):
|
||||
for sub_name, sub_parser in nested_action.choices.items():
|
||||
commands[f"{name} {sub_name}"] = sub_parser
|
||||
return commands
|
||||
|
||||
def get_func(self, args):
|
||||
"""Get the handler function from the dispatch table based on subcommand name."""
|
||||
subcmd = getattr(args, "subcommand", None)
|
||||
if subcmd is None:
|
||||
return functools.partial(self.do_maincommand_help, self.parser)
|
||||
|
||||
subcmd_ns = getattr(args, subcmd, None)
|
||||
nested_subcmd = getattr(subcmd_ns, "subcommand", None) if subcmd_ns else None
|
||||
|
||||
if nested_subcmd is None:
|
||||
method_name = f"do_{subcmd}".replace("-", "_")
|
||||
else:
|
||||
method_name = f"do_{subcmd}_{nested_subcmd}".replace("-", "_")
|
||||
|
||||
func = getattr(self, method_name, None)
|
||||
|
||||
if func is None:
|
||||
# Fallback for container commands or unknown commands
|
||||
if nested_subcmd is None and subcmd_ns is not None:
|
||||
# Might be a container command without a subcommand selected (e.g. just "borg key")
|
||||
subparser = getattr(self, "_commands", {}).get(subcmd)
|
||||
return functools.partial(self.do_subcommand_help, subparser or self.parser)
|
||||
return functools.partial(self.do_maincommand_help, self.parser)
|
||||
|
||||
# Special handling for "help" command which needs extra args
|
||||
if subcmd == "help":
|
||||
func = functools.partial(self.do_help, self.parser, getattr(self, "_commands", {}))
|
||||
|
||||
return func
|
||||
|
||||
def get_args(self, argv, cmd):
|
||||
"""Usually just returns argv, except when dealing with an SSH forced command for borg serve."""
|
||||
result = self.parse_args(argv[1:])
|
||||
if cmd is not None and result.func == self.do_serve:
|
||||
if cmd is not None and self.get_func(result) == self.do_serve:
|
||||
# borg serve case:
|
||||
# - "result" is how borg got invoked (e.g. via forced command from authorized_keys),
|
||||
# - "client_result" (from "cmd") refers to the command the client wanted to execute,
|
||||
|
|
@ -399,7 +439,7 @@ class Archiver(
|
|||
# the borg command line.
|
||||
client_argv = list(itertools.dropwhile(lambda arg: "=" in arg, client_argv))
|
||||
client_result = self.parse_args(client_argv[1:])
|
||||
if client_result.func == result.func:
|
||||
if self.get_func(client_result) == self.get_func(result):
|
||||
# make sure we only process like normal if the client is executing
|
||||
# the same command as specified in the forced command, otherwise
|
||||
# just skip this block and return the forced command (== result).
|
||||
|
|
@ -423,17 +463,24 @@ class Archiver(
|
|||
if args:
|
||||
args = self.preprocess_args(args)
|
||||
parser = self.build_parser()
|
||||
self.parser = parser # save for get_func and help
|
||||
args = parser.parse_args(args or ["-h"])
|
||||
# Flatten jsonargparse's nested namespace into a flat one
|
||||
args = flatten_namespace(args)
|
||||
parser.common_options.resolve(args)
|
||||
func = get_func(args)
|
||||
if func == self.do_create and args.paths and args.paths_from_stdin:
|
||||
func = self.get_func(args)
|
||||
if func == self.do_create and getattr(args, "paths", []) and getattr(args, "paths_from_stdin", False):
|
||||
parser.error("Must not pass PATH with --paths-from-stdin.")
|
||||
if args.progress and getattr(args, "output_list", False) and not args.log_json:
|
||||
if (
|
||||
getattr(args, "progress", False)
|
||||
and getattr(args, "output_list", False)
|
||||
and not getattr(args, "log_json", False)
|
||||
):
|
||||
parser.error("Options --progress and --list do not play nicely together.")
|
||||
if func == self.do_create and not args.paths:
|
||||
if args.content_from_command or args.paths_from_command:
|
||||
if func == self.do_create and not getattr(args, "paths", []):
|
||||
if getattr(args, "content_from_command", False) or getattr(args, "paths_from_command", False):
|
||||
parser.error("No command given.")
|
||||
elif not args.paths_from_stdin:
|
||||
elif not getattr(args, "paths_from_stdin", False):
|
||||
# need at least 1 path but args.paths may also be populated from patterns
|
||||
parser.error("Need at least one PATH argument.")
|
||||
# we can only have a complete knowledge of placeholder replacements we should do **after** arg parsing,
|
||||
|
|
@ -486,7 +533,7 @@ class Archiver(
|
|||
def run(self, args):
|
||||
os.umask(args.umask) # early, before opening files
|
||||
self.lock_wait = args.lock_wait
|
||||
func = get_func(args)
|
||||
func = self.get_func(args)
|
||||
# do not use loggers before this!
|
||||
is_serve = func == self.do_serve
|
||||
self.log_json = args.log_json and not is_serve
|
||||
|
|
@ -542,6 +589,7 @@ class Archiver(
|
|||
else:
|
||||
rc = func(args)
|
||||
assert rc is None
|
||||
|
||||
return get_ec(rc)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import argparse
|
||||
|
||||
from collections import defaultdict
|
||||
import os
|
||||
|
||||
|
|
@ -7,6 +8,7 @@ from ..archive import Archive
|
|||
from ..constants import * # NOQA
|
||||
from ..helpers import bin_to_hex, Error
|
||||
from ..helpers import ProgressIndicatorPercent
|
||||
from ..helpers.jap_wrapper import ArgumentParser
|
||||
from ..manifest import Manifest
|
||||
from ..remote import RemoteRepository
|
||||
from ..repository import Repository
|
||||
|
|
@ -126,14 +128,13 @@ class AnalyzeMixIn:
|
|||
to recreate existing archives without them.
|
||||
"""
|
||||
)
|
||||
subparser = subparsers.add_parser(
|
||||
"analyze",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_analyze.__doc__,
|
||||
epilog=analyze_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="analyze archives",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_analyze)
|
||||
|
||||
subparsers.add_subcommand("analyze", subparser, help="analyze archives")
|
||||
define_archive_filters_group(subparser)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import argparse
|
||||
|
||||
from contextlib import contextmanager
|
||||
import functools
|
||||
import os
|
||||
import tempfile
|
||||
import time
|
||||
|
|
@ -10,6 +10,7 @@ from ..crypto.key import FlexiKey
|
|||
from ..helpers import format_file_size
|
||||
from ..helpers import msgpack
|
||||
from ..helpers import get_reset_ec
|
||||
from ..helpers.jap_wrapper import ArgumentParser
|
||||
from ..item import Item
|
||||
from ..platform import SyncFile
|
||||
|
||||
|
|
@ -250,23 +251,27 @@ class BenchmarkMixIn:
|
|||
spec = "msgpack"
|
||||
print(f"{spec:<12} {size:<10} {timeit(lambda: msgpack.packb(items), number=100):.3f}s")
|
||||
|
||||
def build_parser_benchmarks(self, subparsers, common_parser, mid_common_parser):
|
||||
def build_parser_benchmarks_l1(self, subparsers, mid_common_parser):
|
||||
"""Phase 1: Add the 'benchmark' container subcommand."""
|
||||
from ._common import process_epilog
|
||||
|
||||
benchmark_epilog = process_epilog("These commands do various benchmarks.")
|
||||
|
||||
subparser = subparsers.add_parser(
|
||||
"benchmark",
|
||||
subparser = ArgumentParser(
|
||||
parents=[mid_common_parser],
|
||||
add_help=False,
|
||||
description="benchmark command",
|
||||
epilog=benchmark_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="benchmark command",
|
||||
)
|
||||
subparsers.add_subcommand("benchmark", subparser, help="benchmark command")
|
||||
return subparser
|
||||
|
||||
benchmark_parsers = subparser.add_subparsers(title="required arguments", metavar="<command>")
|
||||
subparser.set_defaults(fallback_func=functools.partial(self.do_subcommand_help, subparser))
|
||||
def build_parser_benchmarks_l2(self, benchmark_parser, common_parser):
|
||||
"""Phase 2: Add leaf subcommands under the 'benchmark' container."""
|
||||
from ._common import process_epilog
|
||||
|
||||
benchmark_parsers = benchmark_parser.add_subcommands(required=False)
|
||||
|
||||
bench_crud_epilog = process_epilog(
|
||||
"""
|
||||
|
|
@ -309,16 +314,16 @@ class BenchmarkMixIn:
|
|||
Try multiple measurements and having a otherwise idle machine (and network, if you use it).
|
||||
"""
|
||||
)
|
||||
subparser = benchmark_parsers.add_parser(
|
||||
"crud",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_benchmark_crud.__doc__,
|
||||
epilog=bench_crud_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="benchmarks Borg CRUD (create, extract, update, delete).",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_benchmark_crud)
|
||||
benchmark_parsers.add_subcommand(
|
||||
"crud", subparser, help="benchmarks Borg CRUD (create, extract, update, delete)."
|
||||
)
|
||||
|
||||
subparser.add_argument("path", metavar="PATH", help="path where to create benchmark input data")
|
||||
|
||||
|
|
@ -333,13 +338,11 @@ class BenchmarkMixIn:
|
|||
- enough free memory so there will be no slow down due to paging activity
|
||||
"""
|
||||
)
|
||||
subparser = benchmark_parsers.add_parser(
|
||||
"cpu",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_benchmark_cpu.__doc__,
|
||||
epilog=bench_cpu_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="benchmarks Borg CPU-bound operations.",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_benchmark_cpu)
|
||||
benchmark_parsers.add_subcommand("cpu", subparser, help="benchmarks Borg CPU-bound operations.")
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import argparse
|
||||
|
||||
from ._common import with_repository, Highlander
|
||||
from ..archive import ArchiveChecker
|
||||
from ..constants import * # NOQA
|
||||
from ..helpers import set_ec, EXIT_WARNING, CancelledByUser, CommandError, IntegrityError
|
||||
from ..helpers import yes
|
||||
from ..helpers.jap_wrapper import ArgumentParser
|
||||
|
||||
from ..logger import create_logger
|
||||
|
||||
|
|
@ -182,16 +184,15 @@ class CheckMixIn:
|
|||
``borg compact`` would remove the archives' data completely.
|
||||
"""
|
||||
)
|
||||
subparser = subparsers.add_parser(
|
||||
"check",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_check.__doc__,
|
||||
epilog=check_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="verify the repository",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_check)
|
||||
|
||||
subparsers.add_subcommand("check", subparser, help="verify the repository")
|
||||
subparser.add_argument(
|
||||
"--repository-only", dest="repo_only", action="store_true", help="only perform repository checks"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import argparse
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from ._common import with_repository
|
||||
|
|
@ -10,6 +11,7 @@ from ..constants import * # NOQA
|
|||
from ..hashindex import ChunkIndex, ChunkIndexEntry
|
||||
from ..helpers import set_ec, EXIT_ERROR, format_file_size, bin_to_hex
|
||||
from ..helpers import ProgressIndicatorPercent
|
||||
from ..helpers.jap_wrapper import ArgumentParser
|
||||
from ..manifest import Manifest
|
||||
from ..remote import RemoteRepository
|
||||
from ..repository import Repository, repo_lister
|
||||
|
|
@ -257,16 +259,15 @@ class CompactMixIn:
|
|||
thus it cannot compute before/after compaction size statistics).
|
||||
"""
|
||||
)
|
||||
subparser = subparsers.add_parser(
|
||||
"compact",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_compact.__doc__,
|
||||
epilog=compact_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="compact the repository",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_compact)
|
||||
|
||||
subparsers.add_subcommand("compact", subparser, help="compact the repository")
|
||||
subparser.add_argument(
|
||||
"-n", "--dry-run", dest="dry_run", action="store_true", help="do not change the repository"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ from ..helpers import (
|
|||
relative_time_marker_validator,
|
||||
parse_file_size,
|
||||
)
|
||||
from ..helpers.jap_wrapper import ArgumentParser
|
||||
from ..helpers.time import timestamp
|
||||
from ..compress import CompressionSpec
|
||||
from ..helpers.parseformat import partial_format
|
||||
|
|
@ -750,16 +751,14 @@ class CompletionMixIn:
|
|||
"""
|
||||
)
|
||||
|
||||
subparser = subparsers.add_parser(
|
||||
"completion",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_completion.__doc__,
|
||||
epilog=completion_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="output shell completion script",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_completion)
|
||||
subparsers.add_subcommand("completion", subparser, help="output shell completion script")
|
||||
subparser.add_argument(
|
||||
"shell", metavar="SHELL", choices=shells, help="shell to generate completion for (one of: %(choices)s)"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import errno
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
import logging
|
||||
import os
|
||||
import posixpath
|
||||
|
|
@ -31,6 +32,7 @@ from ..helpers import sig_int, ignore_sigint
|
|||
from ..helpers import iter_separated
|
||||
from ..helpers import MakePathSafeAction
|
||||
from ..helpers import Error, CommandError, BackupWarning, FileChangedWarning
|
||||
from ..helpers.jap_wrapper import ArgumentParser
|
||||
from ..manifest import Manifest
|
||||
from ..patterns import PatternMatcher
|
||||
from ..platform import is_win32
|
||||
|
|
@ -672,7 +674,6 @@ class CreateMixIn:
|
|||
macOS examples are the apfs mounts of a typical macOS installation.
|
||||
Therefore, when using ``--one-file-system``, you should double-check that the backup works as intended.
|
||||
|
||||
|
||||
.. _list_item_flags:
|
||||
|
||||
Item flags
|
||||
|
|
@ -765,16 +766,15 @@ class CreateMixIn:
|
|||
"""
|
||||
)
|
||||
|
||||
subparser = subparsers.add_parser(
|
||||
"create",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_create.__doc__,
|
||||
epilog=create_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="create a backup",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_create)
|
||||
|
||||
subparsers.add_subcommand("create", subparser, help="create a backup")
|
||||
|
||||
# note: --dry-run and --stats are mutually exclusive, but we do not want to abort when
|
||||
# parsing, but rather proceed with the dry-run, but without stats (see run() method).
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import argparse
|
||||
import functools
|
||||
|
||||
import json
|
||||
import textwrap
|
||||
|
||||
|
|
@ -13,6 +13,7 @@ from ..helpers import dash_open
|
|||
from ..helpers import StableDict
|
||||
from ..helpers import archivename_validator
|
||||
from ..helpers import CommandError, RTError
|
||||
from ..helpers.jap_wrapper import ArgumentParser
|
||||
from ..manifest import Manifest
|
||||
from ..platform import get_process_id
|
||||
from ..repository import Repository, LIST_SCAN_LIMIT, repo_lister
|
||||
|
|
@ -308,7 +309,8 @@ class DebugMixIn:
|
|||
with open(args.output, "wb") as wfd, open(args.input, "rb") as rfd:
|
||||
marshal.dump(msgpack.unpack(rfd, use_list=False, raw=False), wfd)
|
||||
|
||||
def build_parser_debug(self, subparsers, common_parser, mid_common_parser):
|
||||
def build_parser_debug_l1(self, subparsers, mid_common_parser):
|
||||
"""Phase 1: Add the 'debug' container subcommand."""
|
||||
debug_epilog = process_epilog(
|
||||
"""
|
||||
These commands are not intended for normal use and potentially very
|
||||
|
|
@ -319,18 +321,20 @@ class DebugMixIn:
|
|||
what you are doing or if a trusted developer tells you what to do."""
|
||||
)
|
||||
|
||||
subparser = subparsers.add_parser(
|
||||
"debug",
|
||||
subparser = ArgumentParser(
|
||||
parents=[mid_common_parser],
|
||||
add_help=False,
|
||||
description="debugging command (not intended for normal use)",
|
||||
epilog=debug_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="debugging command (not intended for normal use)",
|
||||
)
|
||||
subparsers.add_subcommand("debug", subparser, help="debugging command (not intended for normal use)")
|
||||
return subparser
|
||||
|
||||
debug_parsers = subparser.add_subparsers(title="required arguments", metavar="<command>")
|
||||
subparser.set_defaults(fallback_func=functools.partial(self.do_subcommand_help, subparser))
|
||||
def build_parser_debug_l2(self, debug_parser, common_parser):
|
||||
"""Phase 2: Add leaf subcommands under the 'debug' container."""
|
||||
|
||||
debug_parsers = debug_parser.add_subcommands(required=False)
|
||||
|
||||
debug_info_epilog = process_epilog(
|
||||
"""
|
||||
|
|
@ -339,32 +343,28 @@ class DebugMixIn:
|
|||
already appended at the end of the traceback.
|
||||
"""
|
||||
)
|
||||
subparser = debug_parsers.add_parser(
|
||||
"info",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_debug_info.__doc__,
|
||||
epilog=debug_info_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="show system infos for debugging / bug reports (debug)",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_debug_info)
|
||||
debug_parsers.add_subcommand("info", subparser, help="show system infos for debugging / bug reports (debug)")
|
||||
|
||||
debug_dump_archive_items_epilog = process_epilog(
|
||||
"""
|
||||
This command dumps raw (but decrypted and decompressed) archive items (only metadata) to files.
|
||||
"""
|
||||
)
|
||||
subparser = debug_parsers.add_parser(
|
||||
"dump-archive-items",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_debug_dump_archive_items.__doc__,
|
||||
epilog=debug_dump_archive_items_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="dump archive items (metadata) (debug)",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_debug_dump_archive_items)
|
||||
debug_parsers.add_subcommand("dump-archive-items", subparser, help="dump archive items (metadata) (debug)")
|
||||
subparser.add_argument("name", metavar="NAME", type=archivename_validator, help="specify the archive name")
|
||||
|
||||
debug_dump_archive_epilog = process_epilog(
|
||||
|
|
@ -372,16 +372,14 @@ class DebugMixIn:
|
|||
This command dumps all metadata of an archive in a decoded form to a file.
|
||||
"""
|
||||
)
|
||||
subparser = debug_parsers.add_parser(
|
||||
"dump-archive",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_debug_dump_archive.__doc__,
|
||||
epilog=debug_dump_archive_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="dump decoded archive metadata (debug)",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_debug_dump_archive)
|
||||
debug_parsers.add_subcommand("dump-archive", subparser, help="dump decoded archive metadata (debug)")
|
||||
subparser.add_argument("name", metavar="NAME", type=archivename_validator, help="specify the archive name")
|
||||
subparser.add_argument("path", metavar="PATH", type=str, help="file to dump data into")
|
||||
|
||||
|
|
@ -390,16 +388,14 @@ class DebugMixIn:
|
|||
This command dumps manifest metadata of a repository in a decoded form to a file.
|
||||
"""
|
||||
)
|
||||
subparser = debug_parsers.add_parser(
|
||||
"dump-manifest",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_debug_dump_manifest.__doc__,
|
||||
epilog=debug_dump_manifest_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="dump decoded repository metadata (debug)",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_debug_dump_manifest)
|
||||
debug_parsers.add_subcommand("dump-manifest", subparser, help="dump decoded repository metadata (debug)")
|
||||
subparser.add_argument("path", metavar="PATH", type=str, help="file to dump data into")
|
||||
|
||||
debug_dump_repo_objs_epilog = process_epilog(
|
||||
|
|
@ -407,32 +403,28 @@ class DebugMixIn:
|
|||
This command dumps raw (but decrypted and decompressed) repo objects to files.
|
||||
"""
|
||||
)
|
||||
subparser = debug_parsers.add_parser(
|
||||
"dump-repo-objs",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_debug_dump_repo_objs.__doc__,
|
||||
epilog=debug_dump_repo_objs_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="dump repo objects (debug)",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_debug_dump_repo_objs)
|
||||
debug_parsers.add_subcommand("dump-repo-objs", subparser, help="dump repo objects (debug)")
|
||||
|
||||
debug_search_repo_objs_epilog = process_epilog(
|
||||
"""
|
||||
This command searches raw (but decrypted and decompressed) repo objects for a specific bytes sequence.
|
||||
"""
|
||||
)
|
||||
subparser = debug_parsers.add_parser(
|
||||
"search-repo-objs",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_debug_search_repo_objs.__doc__,
|
||||
epilog=debug_search_repo_objs_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="search repo objects (debug)",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_debug_search_repo_objs)
|
||||
debug_parsers.add_subcommand("search-repo-objs", subparser, help="search repo objects (debug)")
|
||||
subparser.add_argument(
|
||||
"wanted",
|
||||
metavar="WANTED",
|
||||
|
|
@ -445,16 +437,14 @@ class DebugMixIn:
|
|||
This command computes the id-hash for some file content.
|
||||
"""
|
||||
)
|
||||
subparser = debug_parsers.add_parser(
|
||||
"id-hash",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_debug_id_hash.__doc__,
|
||||
epilog=debug_id_hash_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="compute id-hash for some file content (debug)",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_debug_id_hash)
|
||||
debug_parsers.add_subcommand("id-hash", subparser, help="compute id-hash for some file content (debug)")
|
||||
subparser.add_argument(
|
||||
"path", metavar="PATH", type=str, help="content for which the id-hash shall get computed"
|
||||
)
|
||||
|
|
@ -465,16 +455,14 @@ class DebugMixIn:
|
|||
This command parses the object file into metadata (as json) and uncompressed data.
|
||||
"""
|
||||
)
|
||||
subparser = debug_parsers.add_parser(
|
||||
"parse-obj",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_debug_parse_obj.__doc__,
|
||||
epilog=debug_parse_obj_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="parse borg object file into meta dict and data",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_debug_parse_obj)
|
||||
debug_parsers.add_subcommand("parse-obj", subparser, help="parse borg object file into meta dict and data")
|
||||
subparser.add_argument("id", metavar="ID", type=str, help="hex object ID to get from the repo")
|
||||
subparser.add_argument(
|
||||
"object_path", metavar="OBJECT_PATH", type=str, help="path of the object file to parse data from"
|
||||
|
|
@ -492,16 +480,14 @@ class DebugMixIn:
|
|||
This command formats the file and metadata into a Borg object file.
|
||||
"""
|
||||
)
|
||||
subparser = debug_parsers.add_parser(
|
||||
"format-obj",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_debug_format_obj.__doc__,
|
||||
epilog=debug_format_obj_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="format file and metadata into a Borg object file",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_debug_format_obj)
|
||||
debug_parsers.add_subcommand("format-obj", subparser, help="format file and metadata into a Borg object file")
|
||||
subparser.add_argument("id", metavar="ID", type=str, help="hex object ID to get from the repo")
|
||||
subparser.add_argument(
|
||||
"binary_path", metavar="BINARY_PATH", type=str, help="path of the file to convert into an object file"
|
||||
|
|
@ -531,16 +517,14 @@ class DebugMixIn:
|
|||
This command gets an object from the repository.
|
||||
"""
|
||||
)
|
||||
subparser = debug_parsers.add_parser(
|
||||
"get-obj",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_debug_get_obj.__doc__,
|
||||
epilog=debug_get_obj_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="get object from repository (debug)",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_debug_get_obj)
|
||||
debug_parsers.add_subcommand("get-obj", subparser, help="get object from repository (debug)")
|
||||
subparser.add_argument("id", metavar="ID", type=str, help="hex object ID to get from the repo")
|
||||
subparser.add_argument("path", metavar="PATH", type=str, help="file to write object data into")
|
||||
|
||||
|
|
@ -549,16 +533,14 @@ class DebugMixIn:
|
|||
This command puts an object into the repository.
|
||||
"""
|
||||
)
|
||||
subparser = debug_parsers.add_parser(
|
||||
"put-obj",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_debug_put_obj.__doc__,
|
||||
epilog=debug_put_obj_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="put object to repository (debug)",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_debug_put_obj)
|
||||
debug_parsers.add_subcommand("put-obj", subparser, help="put object to repository (debug)")
|
||||
subparser.add_argument("id", metavar="ID", type=str, help="hex object ID to put into the repo")
|
||||
subparser.add_argument("path", metavar="PATH", type=str, help="file to read and create object from")
|
||||
|
||||
|
|
@ -567,16 +549,14 @@ class DebugMixIn:
|
|||
This command deletes objects from the repository.
|
||||
"""
|
||||
)
|
||||
subparser = debug_parsers.add_parser(
|
||||
"delete-obj",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_debug_delete_obj.__doc__,
|
||||
epilog=debug_delete_obj_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="delete object from repository (debug)",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_debug_delete_obj)
|
||||
debug_parsers.add_subcommand("delete-obj", subparser, help="delete object from repository (debug)")
|
||||
subparser.add_argument(
|
||||
"ids", metavar="IDs", nargs="+", type=str, help="hex object ID(s) to delete from the repo"
|
||||
)
|
||||
|
|
@ -586,15 +566,15 @@ class DebugMixIn:
|
|||
Convert a Borg profile to a Python cProfile compatible profile.
|
||||
"""
|
||||
)
|
||||
subparser = debug_parsers.add_parser(
|
||||
"convert-profile",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_debug_convert_profile.__doc__,
|
||||
epilog=debug_convert_profile_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="convert Borg profile to Python profile (debug)",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_debug_convert_profile)
|
||||
debug_parsers.add_subcommand(
|
||||
"convert-profile", subparser, help="convert Borg profile to Python profile (debug)"
|
||||
)
|
||||
subparser.add_argument("input", metavar="INPUT", type=str, help="Borg profile")
|
||||
subparser.add_argument("output", metavar="OUTPUT", type=str, help="Output file")
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import argparse
|
||||
|
||||
import logging
|
||||
|
||||
from ._common import with_repository
|
||||
from ..constants import * # NOQA
|
||||
from ..helpers import format_archive, CommandError, bin_to_hex, archivename_validator
|
||||
from ..helpers.jap_wrapper import ArgumentParser
|
||||
from ..manifest import Manifest
|
||||
|
||||
from ..logger import create_logger
|
||||
|
|
@ -78,16 +80,15 @@ class DeleteMixIn:
|
|||
patterns, see :ref:`borg_patterns`).
|
||||
"""
|
||||
)
|
||||
subparser = subparsers.add_parser(
|
||||
"delete",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_delete.__doc__,
|
||||
epilog=delete_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="delete archives",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_delete)
|
||||
|
||||
subparsers.add_subcommand("delete", subparser, help="delete archives")
|
||||
subparser.add_argument(
|
||||
"-n", "--dry-run", dest="dry_run", action="store_true", help="do not change the repository"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import argparse
|
||||
|
||||
import textwrap
|
||||
import json
|
||||
import sys
|
||||
|
|
@ -9,6 +10,7 @@ from ..archive import Archive
|
|||
from ..constants import * # NOQA
|
||||
from ..helpers import BaseFormatter, DiffFormatter, archivename_validator, PathSpec, BorgJsonEncoder
|
||||
from ..helpers import IncludePatternNeverMatchedWarning, remove_surrogates
|
||||
from ..helpers.jap_wrapper import ArgumentParser
|
||||
from ..item import ItemDiff
|
||||
from ..manifest import Manifest
|
||||
from ..logger import create_logger
|
||||
|
|
@ -203,7 +205,6 @@ class DiffMixIn:
|
|||
|
||||
The following keys are always available:
|
||||
|
||||
|
||||
"""
|
||||
)
|
||||
+ BaseFormatter.keys_help()
|
||||
|
|
@ -293,16 +294,15 @@ class DiffMixIn:
|
|||
raise argparse.ArgumentTypeError(f"unsupported sort field: {field}")
|
||||
return ",".join(parts)
|
||||
|
||||
subparser = subparsers.add_parser(
|
||||
"diff",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_diff.__doc__,
|
||||
epilog=diff_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="find differences in archive contents",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_diff)
|
||||
|
||||
subparsers.add_subcommand("diff", subparser, help="find differences in archive contents")
|
||||
subparser.add_argument(
|
||||
"--numeric-ids",
|
||||
dest="numeric_ids",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import sys
|
||||
import argparse
|
||||
|
||||
import logging
|
||||
import stat
|
||||
|
||||
|
|
@ -12,6 +13,7 @@ from ..helpers import remove_surrogates
|
|||
from ..helpers import HardLinkManager
|
||||
from ..helpers import ProgressIndicatorPercent
|
||||
from ..helpers import BackupWarning, IncludePatternNeverMatchedWarning
|
||||
from ..helpers.jap_wrapper import ArgumentParser
|
||||
from ..manifest import Manifest
|
||||
|
||||
from ..logger import create_logger
|
||||
|
|
@ -154,16 +156,15 @@ class ExtractMixIn:
|
|||
group, permissions, etc.
|
||||
"""
|
||||
)
|
||||
subparser = subparsers.add_parser(
|
||||
"extract",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_extract.__doc__,
|
||||
epilog=extract_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="extract archive contents",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_extract)
|
||||
|
||||
subparsers.add_subcommand("extract", subparser, help="extract archive contents")
|
||||
subparser.add_argument(
|
||||
"--list", dest="output_list", action="store_true", help="output a verbose list of items (files, dirs, ...)"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import collections
|
||||
import functools
|
||||
import textwrap
|
||||
|
||||
from ..constants import * # NOQA
|
||||
from ..helpers.jap_wrapper import ArgumentParser
|
||||
from ..helpers.nanorst import rst_to_terminal
|
||||
|
||||
|
||||
|
|
@ -161,7 +161,6 @@ class HelpMixIn:
|
|||
# not '/home/user/importantjunk' or '/etc/junk':
|
||||
$ borg create -e 'home/*/junk' archive /
|
||||
|
||||
|
||||
# The contents of directories in '/home' are not backed up when their name
|
||||
# ends in '.tmp'
|
||||
$ borg create --exclude 're:^home/[^/]+\\.tmp/' archive /
|
||||
|
|
@ -551,10 +550,8 @@ class HelpMixIn:
|
|||
do_maincommand_help = do_subcommand_help
|
||||
|
||||
def build_parser_help(self, subparsers, common_parser, mid_common_parser, parser):
|
||||
subparser = subparsers.add_parser(
|
||||
"help", parents=[common_parser], add_help=False, description="Extra help", help="Extra help"
|
||||
)
|
||||
subparser = ArgumentParser(parents=[common_parser], add_help=False, description="Extra help")
|
||||
subparsers.add_subcommand("help", subparser, help="Extra help")
|
||||
subparser.add_argument("--epilog-only", dest="epilog_only", action="store_true")
|
||||
subparser.add_argument("--usage-only", dest="usage_only", action="store_true")
|
||||
subparser.set_defaults(func=functools.partial(self.do_help, parser, subparsers.choices))
|
||||
subparser.add_argument("topic", metavar="TOPIC", type=str, nargs="?", help="additional help on TOPIC")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import argparse
|
||||
|
||||
import textwrap
|
||||
from datetime import timedelta
|
||||
|
||||
|
|
@ -6,6 +7,7 @@ from ._common import with_repository
|
|||
from ..archive import Archive
|
||||
from ..constants import * # NOQA
|
||||
from ..helpers import format_timedelta, json_print, basic_json_data, archivename_validator
|
||||
from ..helpers.jap_wrapper import ArgumentParser
|
||||
from ..manifest import Manifest
|
||||
|
||||
from ..logger import create_logger
|
||||
|
|
@ -77,16 +79,15 @@ class InfoMixIn:
|
|||
= all chunks in the repository.
|
||||
"""
|
||||
)
|
||||
subparser = subparsers.add_parser(
|
||||
"info",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_info.__doc__,
|
||||
epilog=info_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="show repository or archive information",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_info)
|
||||
|
||||
subparsers.add_subcommand("info", subparser, help="show repository or archive information")
|
||||
subparser.add_argument("--json", action="store_true", help="format output as JSON")
|
||||
define_archive_filters_group(subparser)
|
||||
subparser.add_argument(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import argparse
|
||||
import functools
|
||||
|
||||
import os
|
||||
|
||||
from ..constants import * # NOQA
|
||||
|
|
@ -7,6 +7,7 @@ from ..crypto.key import AESOCBRepoKey, CHPORepoKey, Blake2AESOCBRepoKey, Blake2
|
|||
from ..crypto.key import AESOCBKeyfileKey, CHPOKeyfileKey, Blake2AESOCBKeyfileKey, Blake2CHPOKeyfileKey
|
||||
from ..crypto.keymanager import KeyManager
|
||||
from ..helpers import PathSpec, CommandError
|
||||
from ..helpers.jap_wrapper import ArgumentParser
|
||||
from ..manifest import Manifest
|
||||
|
||||
from ._common import with_repository
|
||||
|
|
@ -18,7 +19,7 @@ logger = create_logger(__name__)
|
|||
|
||||
class KeysMixIn:
|
||||
@with_repository(compatibility=(Manifest.Operation.CHECK,))
|
||||
def do_change_passphrase(self, args, repository, manifest):
|
||||
def do_key_change_passphrase(self, args, repository, manifest):
|
||||
"""Changes the repository key file passphrase."""
|
||||
key = manifest.key
|
||||
if not hasattr(key, "change_passphrase"):
|
||||
|
|
@ -30,7 +31,7 @@ class KeysMixIn:
|
|||
logger.info("Key location: %s", key.find_key())
|
||||
|
||||
@with_repository(exclusive=True, manifest=True, cache=True, compatibility=(Manifest.Operation.CHECK,))
|
||||
def do_change_location(self, args, repository, manifest, cache):
|
||||
def do_key_change_location(self, args, repository, manifest, cache):
|
||||
"""Changes the repository key location."""
|
||||
key = manifest.key
|
||||
if not hasattr(key, "change_passphrase"):
|
||||
|
|
@ -117,21 +118,23 @@ class KeysMixIn:
|
|||
raise CommandError(f"input file does not exist: {args.path}")
|
||||
manager.import_keyfile(args)
|
||||
|
||||
def build_parser_keys(self, subparsers, common_parser, mid_common_parser):
|
||||
from ._common import process_epilog
|
||||
|
||||
subparser = subparsers.add_parser(
|
||||
"key",
|
||||
def build_parser_keys_l1(self, subparsers, mid_common_parser):
|
||||
"""Phase 1: Add the 'key' container subcommand."""
|
||||
subparser = ArgumentParser(
|
||||
parents=[mid_common_parser],
|
||||
add_help=False,
|
||||
description="Manage the keyfile or repokey of a repository",
|
||||
epilog="",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="manage the repository key",
|
||||
)
|
||||
subparsers.add_subcommand("key", subparser, help="manage the repository key")
|
||||
return subparser
|
||||
|
||||
key_parsers = subparser.add_subparsers(title="required arguments", metavar="<command>")
|
||||
subparser.set_defaults(fallback_func=functools.partial(self.do_subcommand_help, subparser))
|
||||
def build_parser_keys_l2(self, key_parser, common_parser):
|
||||
"""Phase 2: Add leaf subcommands under the 'key' container."""
|
||||
from ._common import process_epilog
|
||||
|
||||
key_parsers = key_parser.add_subcommands(required=False)
|
||||
|
||||
key_export_epilog = process_epilog(
|
||||
"""
|
||||
|
|
@ -164,16 +167,14 @@ class KeysMixIn:
|
|||
HTML template with a QR code and a copy of the ``--paper``-formatted key.
|
||||
"""
|
||||
)
|
||||
subparser = key_parsers.add_parser(
|
||||
"export",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_key_export.__doc__,
|
||||
epilog=key_export_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="export the repository key for backup",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_key_export)
|
||||
key_parsers.add_subcommand("export", subparser, help="export the repository key for backup")
|
||||
subparser.add_argument("path", metavar="PATH", nargs="?", type=PathSpec, help="where to store the backup")
|
||||
subparser.add_argument(
|
||||
"--paper",
|
||||
|
|
@ -206,16 +207,14 @@ class KeysMixIn:
|
|||
key import`` creates a new key file in ``$BORG_KEYS_DIR``.
|
||||
"""
|
||||
)
|
||||
subparser = key_parsers.add_parser(
|
||||
"import",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_key_import.__doc__,
|
||||
epilog=key_import_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="import the repository key from backup",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_key_import)
|
||||
key_parsers.add_subcommand("import", subparser, help="import the repository key from backup")
|
||||
subparser.add_argument(
|
||||
"path", metavar="PATH", nargs="?", type=PathSpec, help="path to the backup ('-' to read from stdin)"
|
||||
)
|
||||
|
|
@ -237,16 +236,14 @@ class KeysMixIn:
|
|||
does not protect future (nor past) backups to the same repository.
|
||||
"""
|
||||
)
|
||||
subparser = key_parsers.add_parser(
|
||||
"change-passphrase",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_change_passphrase.__doc__,
|
||||
description=self.do_key_change_passphrase.__doc__,
|
||||
epilog=change_passphrase_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="change the repository passphrase",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_change_passphrase)
|
||||
key_parsers.add_subcommand("change-passphrase", subparser, help="change the repository passphrase")
|
||||
|
||||
change_location_epilog = process_epilog(
|
||||
"""
|
||||
|
|
@ -261,16 +258,14 @@ class KeysMixIn:
|
|||
thus you must ONLY give the key location (keyfile or repokey).
|
||||
"""
|
||||
)
|
||||
subparser = key_parsers.add_parser(
|
||||
"change-location",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_change_location.__doc__,
|
||||
description=self.do_key_change_location.__doc__,
|
||||
epilog=change_location_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="change the key location",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_change_location)
|
||||
key_parsers.add_subcommand("change-location", subparser, help="change the key location")
|
||||
subparser.add_argument(
|
||||
"key_mode", metavar="KEY_LOCATION", choices=("repokey", "keyfile"), help="select key location"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import argparse
|
||||
|
||||
import os
|
||||
import textwrap
|
||||
import sys
|
||||
|
|
@ -8,6 +9,7 @@ from ..archive import Archive
|
|||
from ..cache import Cache
|
||||
from ..constants import * # NOQA
|
||||
from ..helpers import ItemFormatter, BaseFormatter, archivename_validator, PathSpec
|
||||
from ..helpers.jap_wrapper import ArgumentParser
|
||||
from ..manifest import Manifest
|
||||
|
||||
from ..logger import create_logger
|
||||
|
|
@ -89,7 +91,6 @@ class ListMixIn:
|
|||
|
||||
The following keys are always available:
|
||||
|
||||
|
||||
"""
|
||||
)
|
||||
+ BaseFormatter.keys_help()
|
||||
|
|
@ -102,16 +103,15 @@ class ListMixIn:
|
|||
)
|
||||
+ ItemFormatter.keys_help()
|
||||
)
|
||||
subparser = subparsers.add_parser(
|
||||
"list",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_list.__doc__,
|
||||
epilog=list_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="list archive contents",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_list)
|
||||
|
||||
subparsers.add_subcommand("list", subparser, help="list archive contents")
|
||||
subparser.add_argument(
|
||||
"--short", dest="short", action="store_true", help="only print file/directory names, nothing else"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import argparse
|
||||
|
||||
import subprocess
|
||||
|
||||
from ._common import with_repository
|
||||
from ..cache import Cache
|
||||
from ..constants import * # NOQA
|
||||
from ..helpers import prepare_subprocess_env, set_ec, CommandError, ThreadRunner
|
||||
from ..helpers.jap_wrapper import ArgumentParser
|
||||
|
||||
from ..logger import create_logger
|
||||
|
||||
|
|
@ -45,16 +47,15 @@ class LocksMixIn:
|
|||
trying to access the cache or the repository.
|
||||
"""
|
||||
)
|
||||
subparser = subparsers.add_parser(
|
||||
"break-lock",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_break_lock.__doc__,
|
||||
epilog=break_lock_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="break the repository and cache locks",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_break_lock)
|
||||
|
||||
subparsers.add_subcommand("break-lock", subparser, help="break the repository and cache locks")
|
||||
|
||||
with_lock_epilog = process_epilog(
|
||||
"""
|
||||
|
|
@ -77,15 +78,14 @@ class LocksMixIn:
|
|||
Borg is cautious and does not automatically remove stale locks made by a different host.
|
||||
"""
|
||||
)
|
||||
subparser = subparsers.add_parser(
|
||||
"with-lock",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_with_lock.__doc__,
|
||||
epilog=with_lock_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="run a user command with the lock held",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_with_lock)
|
||||
|
||||
subparsers.add_subcommand("with-lock", subparser, help="run a user command with the lock held")
|
||||
subparser.add_argument("command", metavar="COMMAND", help="command to run")
|
||||
subparser.add_argument("args", metavar="ARGS", nargs=argparse.REMAINDER, help="command arguments")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import argparse
|
||||
|
||||
import os
|
||||
|
||||
from ._common import with_repository, Highlander
|
||||
|
|
@ -6,6 +7,7 @@ from ..constants import * # NOQA
|
|||
from ..helpers import RTError
|
||||
from ..helpers import PathSpec
|
||||
from ..helpers import umount
|
||||
from ..helpers.jap_wrapper import ArgumentParser
|
||||
from ..manifest import Manifest
|
||||
from ..remote import cache_if_remote
|
||||
|
||||
|
|
@ -151,15 +153,15 @@ class MountMixIn:
|
|||
the logger to output to a file.
|
||||
"""
|
||||
)
|
||||
subparser = subparsers.add_parser(
|
||||
"mount",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_mount.__doc__,
|
||||
epilog=mount_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="mount a repository",
|
||||
)
|
||||
|
||||
subparsers.add_subcommand("mount", subparser, help="mount a repository")
|
||||
self._define_borg_mount(subparser)
|
||||
|
||||
umount_epilog = process_epilog(
|
||||
|
|
@ -170,16 +172,15 @@ class MountMixIn:
|
|||
command - usually this is either umount or fusermount -u.
|
||||
"""
|
||||
)
|
||||
subparser = subparsers.add_parser(
|
||||
"umount",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_umount.__doc__,
|
||||
epilog=umount_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="unmount a repository",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_umount)
|
||||
|
||||
subparsers.add_subcommand("umount", subparser, help="unmount a repository")
|
||||
subparser.add_argument(
|
||||
"mountpoint", metavar="MOUNTPOINT", type=str, help="mountpoint of the filesystem to unmount"
|
||||
)
|
||||
|
|
@ -196,7 +197,6 @@ class MountMixIn:
|
|||
def _define_borg_mount(self, parser):
|
||||
from ._common import define_exclusion_group, define_archive_filters_group
|
||||
|
||||
parser.set_defaults(func=self.do_mount)
|
||||
parser.add_argument("mountpoint", metavar="MOUNTPOINT", type=str, help="where to mount the filesystem")
|
||||
parser.add_argument(
|
||||
"-f", "--foreground", dest="foreground", action="store_true", help="stay in foreground, do not daemonize"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import argparse
|
||||
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime, timezone, timedelta
|
||||
import logging
|
||||
|
|
@ -11,6 +12,7 @@ from ..cache import Cache
|
|||
from ..constants import * # NOQA
|
||||
from ..helpers import ArchiveFormatter, interval, sig_int, ProgressIndicatorPercent, CommandError, Error
|
||||
from ..helpers import archivename_validator
|
||||
from ..helpers.jap_wrapper import ArgumentParser
|
||||
from ..manifest import Manifest
|
||||
|
||||
from ..logger import create_logger
|
||||
|
|
@ -273,16 +275,15 @@ class PruneMixIn:
|
|||
the ``borg repo-list`` description for more details about the format string).
|
||||
"""
|
||||
)
|
||||
subparser = subparsers.add_parser(
|
||||
"prune",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_prune.__doc__,
|
||||
epilog=prune_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="prune archives",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_prune)
|
||||
|
||||
subparsers.add_subcommand("prune", subparser, help="prune archives")
|
||||
subparser.add_argument(
|
||||
"-n", "--dry-run", dest="dry_run", action="store_true", help="do not change the repository"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from ..constants import * # NOQA
|
|||
from ..compress import CompressionSpec
|
||||
from ..helpers import archivename_validator, comment_validator, PathSpec, ChunkerParams, bin_to_hex
|
||||
from ..helpers import timestamp
|
||||
from ..helpers.jap_wrapper import ArgumentParser
|
||||
from ..manifest import Manifest
|
||||
|
||||
from ..logger import create_logger
|
||||
|
|
@ -101,16 +102,15 @@ class RecreateMixIn:
|
|||
if the chunks are still missing.
|
||||
"""
|
||||
)
|
||||
subparser = subparsers.add_parser(
|
||||
"recreate",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_recreate.__doc__,
|
||||
epilog=recreate_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help=self.do_recreate.__doc__,
|
||||
)
|
||||
subparser.set_defaults(func=self.do_recreate)
|
||||
|
||||
subparsers.add_subcommand("recreate", subparser, help=self.do_recreate.__doc__)
|
||||
subparser.add_argument(
|
||||
"--list", dest="output_list", action="store_true", help="output verbose list of items (files, dirs, ...)"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import argparse
|
|||
from ._common import with_repository, with_archive
|
||||
from ..constants import * # NOQA
|
||||
from ..helpers import archivename_validator
|
||||
from ..helpers.jap_wrapper import ArgumentParser
|
||||
from ..manifest import Manifest
|
||||
|
||||
from ..logger import create_logger
|
||||
|
|
@ -28,16 +29,15 @@ class RenameMixIn:
|
|||
This results in a different archive ID.
|
||||
"""
|
||||
)
|
||||
subparser = subparsers.add_parser(
|
||||
"rename",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_rename.__doc__,
|
||||
epilog=rename_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="rename an archive",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_rename)
|
||||
|
||||
subparsers.add_subcommand("rename", subparser, help="rename an archive")
|
||||
subparser.add_argument(
|
||||
"name", metavar="OLDNAME", type=archivename_validator, help="specify the current archive name"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import argparse
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from ._common import with_repository, Highlander
|
||||
|
|
@ -6,6 +7,7 @@ from ..constants import * # NOQA
|
|||
from ..compress import CompressionSpec, ObfuscateSize, Auto, COMPRESSOR_TABLE
|
||||
from ..hashindex import ChunkIndex
|
||||
from ..helpers import sig_int, ProgressIndicatorPercent, Error
|
||||
from ..helpers.jap_wrapper import ArgumentParser
|
||||
from ..repository import Repository
|
||||
from ..remote import RemoteRepository
|
||||
from ..manifest import Manifest
|
||||
|
|
@ -180,16 +182,15 @@ class RepoCompressMixIn:
|
|||
You do **not** need to run ``borg compact`` after ``borg repo-compress``.
|
||||
"""
|
||||
)
|
||||
subparser = subparsers.add_parser(
|
||||
"repo-compress",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_repo_compress.__doc__,
|
||||
epilog=repo_compress_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help=self.do_repo_compress.__doc__,
|
||||
)
|
||||
subparser.set_defaults(func=self.do_repo_compress)
|
||||
|
||||
subparsers.add_subcommand("repo-compress", subparser, help=self.do_repo_compress.__doc__)
|
||||
|
||||
subparser.add_argument(
|
||||
"-C",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from ..constants import * # NOQA
|
|||
from ..crypto.key import key_creator, key_argument_names
|
||||
from ..helpers import CancelledByUser
|
||||
from ..helpers import location_validator, Location
|
||||
from ..helpers.jap_wrapper import ArgumentParser
|
||||
from ..manifest import Manifest
|
||||
|
||||
from ..logger import create_logger
|
||||
|
|
@ -190,16 +191,15 @@ class RepoCreateMixIn:
|
|||
Then use ``borg transfer --other-repo ORIG_REPO --from-borg1 ...`` to transfer the archives.
|
||||
"""
|
||||
)
|
||||
subparser = subparsers.add_parser(
|
||||
"repo-create",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_repo_create.__doc__,
|
||||
epilog=repo_create_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="create a new, empty repository",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_repo_create)
|
||||
|
||||
subparsers.add_subcommand("repo-create", subparser, help="create a new, empty repository")
|
||||
subparser.add_argument(
|
||||
"--other-repo",
|
||||
metavar="SRC_REPOSITORY",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from ..helpers import CancelledByUser
|
|||
from ..helpers import format_archive
|
||||
from ..helpers import bin_to_hex
|
||||
from ..helpers import yes
|
||||
from ..helpers.jap_wrapper import ArgumentParser
|
||||
from ..manifest import Manifest, NoManifestError
|
||||
|
||||
from ..logger import create_logger
|
||||
|
|
@ -102,16 +103,15 @@ class RepoDeleteMixIn:
|
|||
Always first use ``--dry-run --list`` to see what would be deleted.
|
||||
"""
|
||||
)
|
||||
subparser = subparsers.add_parser(
|
||||
"repo-delete",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_repo_delete.__doc__,
|
||||
epilog=repo_delete_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="delete a repository",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_repo_delete)
|
||||
|
||||
subparsers.add_subcommand("repo-delete", subparser, help="delete a repository")
|
||||
subparser.add_argument(
|
||||
"-n", "--dry-run", dest="dry_run", action="store_true", help="do not change the repository"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import argparse
|
||||
|
||||
import textwrap
|
||||
|
||||
from ._common import with_repository
|
||||
from ..constants import * # NOQA
|
||||
from ..helpers import bin_to_hex, json_print, basic_json_data
|
||||
from ..helpers.jap_wrapper import ArgumentParser
|
||||
from ..manifest import Manifest
|
||||
|
||||
from ..logger import create_logger
|
||||
|
|
@ -63,14 +65,13 @@ class RepoInfoMixIn:
|
|||
This command displays detailed information about the repository.
|
||||
"""
|
||||
)
|
||||
subparser = subparsers.add_parser(
|
||||
"repo-info",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_repo_info.__doc__,
|
||||
epilog=repo_info_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="show repository information",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_repo_info)
|
||||
|
||||
subparsers.add_subcommand("repo-info", subparser, help="show repository information")
|
||||
subparser.add_argument("--json", action="store_true", help="format output as JSON")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import argparse
|
||||
|
||||
import os
|
||||
import textwrap
|
||||
import sys
|
||||
|
|
@ -6,6 +7,7 @@ import sys
|
|||
from ._common import with_repository, Highlander
|
||||
from ..constants import * # NOQA
|
||||
from ..helpers import BaseFormatter, ArchiveFormatter, json_print, basic_json_data
|
||||
from ..helpers.jap_wrapper import ArgumentParser
|
||||
from ..manifest import Manifest
|
||||
|
||||
from ..logger import create_logger
|
||||
|
|
@ -72,7 +74,6 @@ class RepoListMixIn:
|
|||
|
||||
The following keys are always available:
|
||||
|
||||
|
||||
"""
|
||||
)
|
||||
+ BaseFormatter.keys_help()
|
||||
|
|
@ -85,16 +86,15 @@ class RepoListMixIn:
|
|||
)
|
||||
+ ArchiveFormatter.keys_help()
|
||||
)
|
||||
subparser = subparsers.add_parser(
|
||||
"repo-list",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_repo_list.__doc__,
|
||||
epilog=repo_list_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="list repository contents",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_repo_list)
|
||||
|
||||
subparsers.add_subcommand("repo-list", subparser, help="list repository contents")
|
||||
subparser.add_argument(
|
||||
"--short", dest="short", action="store_true", help="only print the archive IDs, nothing else"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import argparse
|
||||
|
||||
import math
|
||||
import os
|
||||
|
||||
|
|
@ -7,6 +8,7 @@ from borgstore.store import ItemInfo
|
|||
from ._common import with_repository, Highlander
|
||||
from ..constants import * # NOQA
|
||||
from ..helpers import parse_file_size, format_file_size
|
||||
from ..helpers.jap_wrapper import ArgumentParser
|
||||
|
||||
from ..logger import create_logger
|
||||
|
||||
|
|
@ -82,20 +84,18 @@ class RepoSpaceMixIn:
|
|||
$ borg compact -v # only this actually frees space of deleted archives
|
||||
$ borg repo-space --reserve 1G # reserve space again for next time
|
||||
|
||||
|
||||
Reserved space is always rounded up to full reservation blocks of 64 MiB.
|
||||
"""
|
||||
)
|
||||
subparser = subparsers.add_parser(
|
||||
"repo-space",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_repo_space.__doc__,
|
||||
epilog=repo_space_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="manage reserved space in a repository",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_repo_space)
|
||||
|
||||
subparsers.add_subcommand("repo-space", subparser, help="manage reserved space in a repository")
|
||||
subparser.add_argument(
|
||||
"--reserve",
|
||||
metavar="SPACE",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import argparse
|
||||
|
||||
from ..constants import * # NOQA
|
||||
from ..helpers.jap_wrapper import ArgumentParser
|
||||
from ..remote import RepositoryServer
|
||||
|
||||
from ..logger import create_logger
|
||||
|
|
@ -52,16 +53,15 @@ class ServeMixIn:
|
|||
Existing archives can be read, but no archives can be created or deleted.
|
||||
"""
|
||||
)
|
||||
subparser = subparsers.add_parser(
|
||||
"serve",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_serve.__doc__,
|
||||
epilog=serve_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="start the repository server process",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_serve)
|
||||
|
||||
subparsers.add_subcommand("serve", subparser, help="start the repository server process")
|
||||
subparser.add_argument(
|
||||
"--restrict-to-path",
|
||||
metavar="PATH",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,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, Error
|
||||
from ..helpers.jap_wrapper import ArgumentParser
|
||||
from ..manifest import Manifest
|
||||
|
||||
from ..logger import create_logger
|
||||
|
|
@ -80,16 +81,15 @@ class TagMixIn:
|
|||
removed).
|
||||
"""
|
||||
)
|
||||
subparser = subparsers.add_parser(
|
||||
"tag",
|
||||
subparser = ArgumentParser(
|
||||
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)
|
||||
|
||||
subparsers.add_subcommand("tag", subparser, help="tag archives")
|
||||
subparser.add_argument(
|
||||
"--set",
|
||||
dest="set_tags",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import argparse
|
||||
|
||||
import base64
|
||||
import logging
|
||||
import os
|
||||
|
|
@ -20,6 +21,7 @@ from ..helpers import remove_surrogates
|
|||
from ..helpers import timestamp, archive_ts_now
|
||||
from ..helpers import basic_json_data, json_print
|
||||
from ..helpers import log_multi
|
||||
from ..helpers.jap_wrapper import ArgumentParser
|
||||
from ..manifest import Manifest
|
||||
|
||||
from ._common import with_repository, with_archive, Highlander, define_exclusion_group
|
||||
|
|
@ -386,16 +388,15 @@ class TarMixIn:
|
|||
pass over the archive metadata.
|
||||
"""
|
||||
)
|
||||
subparser = subparsers.add_parser(
|
||||
"export-tar",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_export_tar.__doc__,
|
||||
epilog=export_tar_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="create tarball from archive",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_export_tar)
|
||||
|
||||
subparsers.add_subcommand("export-tar", subparser, help="create tarball from archive")
|
||||
subparser.add_argument(
|
||||
"--tar-filter",
|
||||
dest="tar_filter",
|
||||
|
|
@ -462,16 +463,15 @@ class TarMixIn:
|
|||
``--ignore-zeros`` option to skip through the stop markers between them.
|
||||
"""
|
||||
)
|
||||
subparser = subparsers.add_parser(
|
||||
"import-tar",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_import_tar.__doc__,
|
||||
epilog=import_tar_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help=self.do_import_tar.__doc__,
|
||||
)
|
||||
subparser.set_defaults(func=self.do_import_tar)
|
||||
|
||||
subparsers.add_subcommand("import-tar", subparser, help=self.do_import_tar.__doc__)
|
||||
subparser.add_argument(
|
||||
"--tar-filter",
|
||||
dest="tar_filter",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from ..helpers import Error
|
|||
from ..helpers import location_validator, Location, archivename_validator, comment_validator
|
||||
from ..helpers import format_file_size, bin_to_hex
|
||||
from ..helpers import ChunkerParams, ChunkIteratorFileWrapper
|
||||
from ..helpers.jap_wrapper import ArgumentParser
|
||||
from ..item import ChunkListEntry
|
||||
from ..manifest import Manifest
|
||||
from ..legacyrepository import LegacyRepository
|
||||
|
|
@ -309,7 +310,6 @@ class TransferMixIn:
|
|||
borg --repo=DST_REPO transfer --other-repo=SRC_REPO # do it!
|
||||
borg --repo=DST_REPO transfer --other-repo=SRC_REPO --dry-run # check! anything left?
|
||||
|
||||
|
||||
Data migration / upgrade from borg 1.x
|
||||
++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
|
|
@ -330,19 +330,17 @@ class TransferMixIn:
|
|||
borg --repo=DST_REPO transfer --other-repo=SRC_REPO \\
|
||||
--chunker-params=buzhash,19,23,21,4095
|
||||
|
||||
|
||||
"""
|
||||
)
|
||||
subparser = subparsers.add_parser(
|
||||
"transfer",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_transfer.__doc__,
|
||||
epilog=transfer_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="transfer of archives from another repository",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_transfer)
|
||||
|
||||
subparsers.add_subcommand("transfer", subparser, help="transfer of archives from another repository")
|
||||
subparser.add_argument(
|
||||
"-n", "--dry-run", dest="dry_run", action="store_true", help="do not change repository, just check"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import argparse
|
||||
|
||||
import logging
|
||||
|
||||
from ._common import with_repository
|
||||
from ..constants import * # NOQA
|
||||
from ..helpers import format_archive, CommandError, bin_to_hex, archivename_validator
|
||||
from ..helpers.jap_wrapper import ArgumentParser
|
||||
from ..manifest import Manifest
|
||||
|
||||
from ..logger import create_logger
|
||||
|
|
@ -72,16 +74,15 @@ class UnDeleteMixIn:
|
|||
patterns, see :ref:`borg_patterns`).
|
||||
"""
|
||||
)
|
||||
subparser = subparsers.add_parser(
|
||||
"undelete",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_undelete.__doc__,
|
||||
epilog=undelete_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="undelete archives",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_undelete)
|
||||
|
||||
subparsers.add_subcommand("undelete", subparser, help="undelete archives")
|
||||
subparser.add_argument(
|
||||
"-n", "--dry-run", dest="dry_run", action="store_true", help="do not change the repository"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import argparse
|
|||
|
||||
from .. import __version__
|
||||
from ..constants import * # NOQA
|
||||
from ..helpers.jap_wrapper import ArgumentParser
|
||||
from ..remote import RemoteRepository
|
||||
|
||||
from ..logger import create_logger
|
||||
|
|
@ -51,13 +52,11 @@ class VersionMixIn:
|
|||
You can also use ``borg --version`` to display a potentially more precise client version.
|
||||
"""
|
||||
)
|
||||
subparser = subparsers.add_parser(
|
||||
"version",
|
||||
subparser = ArgumentParser(
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_version.__doc__,
|
||||
epilog=version_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="display the Borg client and server versions",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_version)
|
||||
subparsers.add_subcommand("version", subparser, help="display the Borg client and server versions")
|
||||
|
|
|
|||
178
src/borg/helpers/jap_wrapper.py
Normal file
178
src/borg/helpers/jap_wrapper.py
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
"""Borg-specific ArgumentParser wrapping jsonargparse.
|
||||
|
||||
This module provides a compatibility layer between Borg's argparse patterns
|
||||
and jsonargparse's API. Key adaptations:
|
||||
|
||||
1. type+action combination: jsonargparse forbids combining type= and action=
|
||||
in add_argument(). Our override strips type= from kwargs and ensures
|
||||
type conversion happens within the action itself:
|
||||
- Highlander action class: handles type via _type_fn (pops type in __init__)
|
||||
- Standard actions (append, store, etc.): wrapped in TypeConvertingAction
|
||||
- Custom action classes: type is popped and stored on the action after creation
|
||||
|
||||
2. Namespace flattening: jsonargparse creates nested namespaces for subcommands
|
||||
(args.create.name instead of args.name). flatten_namespace() merges these
|
||||
into a flat namespace compatible with Borg's command handlers.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
|
||||
from jsonargparse import ArgumentParser as _JAPArgumentParser
|
||||
from jsonargparse._core import ArgumentGroup as _JAPArgumentGroup
|
||||
|
||||
|
||||
def _is_highlander_action(action):
|
||||
"""Check if action is a Highlander subclass."""
|
||||
try:
|
||||
from .parseformat import Highlander
|
||||
except ImportError:
|
||||
return False
|
||||
return isinstance(action, type) and issubclass(action, Highlander)
|
||||
|
||||
|
||||
def _make_type_converting_action(base_action_name, type_fn):
|
||||
"""Create a custom action class that wraps a standard action and applies type conversion.
|
||||
|
||||
This is used for standard string actions (e.g. 'append', 'store') when combined with type=.
|
||||
jsonargparse forbids type+action, so we strip type= and wrap the action to do conversion.
|
||||
"""
|
||||
# Map action name to argparse's built-in action class
|
||||
_action_map = {"append": argparse._AppendAction, "store": argparse._StoreAction}
|
||||
|
||||
base_cls = _action_map.get(base_action_name)
|
||||
if base_cls is None:
|
||||
# Unknown action string - can't wrap it
|
||||
return None
|
||||
|
||||
class TypeConvertingAction(base_cls):
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
if type_fn is not None and isinstance(values, str):
|
||||
try:
|
||||
values = type_fn(values)
|
||||
except argparse.ArgumentTypeError as e:
|
||||
raise argparse.ArgumentError(self, str(e))
|
||||
super().__call__(parser, namespace, values, option_string)
|
||||
|
||||
TypeConvertingAction.__name__ = f"TypeConverting{base_action_name.title()}Action"
|
||||
return TypeConvertingAction
|
||||
|
||||
|
||||
class BorgAddArgumentMixin:
|
||||
"""Mixin to provide Borg's add_argument logic to ArgumentParser and ArgumentGroup."""
|
||||
|
||||
def add_argument(self, *args, **kwargs):
|
||||
"""Handle type+action combination that jsonargparse forbids.
|
||||
|
||||
jsonargparse raises ValueError when both type= and action= are given.
|
||||
We strip type= from kwargs and ensure the action handles type conversion:
|
||||
- Highlander/subclasses: type bound as class attribute _type_fn_override
|
||||
- Standard string actions: wrapped in TypeConvertingAction
|
||||
- Other custom actions: type stored as _type_fn on action instance
|
||||
"""
|
||||
action = kwargs.get("action")
|
||||
if action is not None and "type" in kwargs:
|
||||
type_fn = kwargs.pop("type")
|
||||
|
||||
if _is_highlander_action(action):
|
||||
# Create a dynamic subclass with _type_fn pre-bound as a class attribute.
|
||||
# Highlander's __init__ will pick this up.
|
||||
action_cls = action
|
||||
|
||||
class BoundHighlander(action_cls):
|
||||
_type_fn_override = type_fn
|
||||
|
||||
BoundHighlander.__name__ = action_cls.__name__
|
||||
BoundHighlander.__qualname__ = action_cls.__qualname__
|
||||
kwargs["action"] = BoundHighlander
|
||||
return super().add_argument(*args, **kwargs)
|
||||
|
||||
if isinstance(action, str):
|
||||
# Standard action string like 'append', 'store'
|
||||
wrapper = _make_type_converting_action(action, type_fn)
|
||||
if wrapper is not None:
|
||||
kwargs["action"] = wrapper
|
||||
return super().add_argument(*args, **kwargs)
|
||||
else:
|
||||
# Unknown standard action, put type back and try anyway
|
||||
kwargs["type"] = type_fn
|
||||
|
||||
elif isinstance(action, type) and issubclass(action, argparse.Action):
|
||||
# Custom action class - register without type, then patch the action
|
||||
result = super().add_argument(*args, **kwargs)
|
||||
# Store type_fn on the action for potential manual use
|
||||
if hasattr(result, "_type_fn"):
|
||||
pass # already handled
|
||||
else:
|
||||
result._type_fn = type_fn
|
||||
return result
|
||||
|
||||
return super().add_argument(*args, **kwargs)
|
||||
|
||||
|
||||
class ArgumentGroup(BorgAddArgumentMixin, _JAPArgumentGroup):
|
||||
"""ArgumentGroup that supports Borg's add_argument patterns."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ArgumentParser(BorgAddArgumentMixin, _JAPArgumentParser):
|
||||
"""ArgumentParser bridging Borg's argparse patterns with jsonargparse."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Force jsonargparse to use our ArgumentGroup class instead of trying to
|
||||
# auto-generate one from source code (which is fragile and fails on Windows CI).
|
||||
self._group_class = ArgumentGroup
|
||||
|
||||
|
||||
def flatten_namespace(args):
|
||||
"""Flatten jsonargparse's nested namespace into a flat one.
|
||||
|
||||
jsonargparse creates nested namespaces for subcommands:
|
||||
args.subcommand = "create"
|
||||
args.create = Namespace(name="myarchive", ...)
|
||||
|
||||
Borg expects a flat namespace:
|
||||
args.name = "myarchive"
|
||||
|
||||
For nested subcommands (key export, debug info, benchmark crud):
|
||||
args.subcommand = "key"
|
||||
args.key.subcommand = "export"
|
||||
args.key.export = Namespace(path="/tmp/k", ...)
|
||||
becomes:
|
||||
args.subcommand = "key"
|
||||
args.path = "/tmp/k"
|
||||
"""
|
||||
subcmd = getattr(args, "subcommand", None)
|
||||
if subcmd is None:
|
||||
return args
|
||||
|
||||
subcmd_ns = getattr(args, subcmd, None)
|
||||
if subcmd_ns is None:
|
||||
return args
|
||||
|
||||
# Handle nested subcommand (e.g., "key export")
|
||||
nested_subcmd = getattr(subcmd_ns, "subcommand", None)
|
||||
if nested_subcmd is not None:
|
||||
nested_ns = getattr(subcmd_ns, nested_subcmd, None)
|
||||
if nested_ns is not None:
|
||||
for key, val in vars(nested_ns).items():
|
||||
if key != "subcommand":
|
||||
setattr(args, key, val)
|
||||
|
||||
# Flatten the direct subcommand namespace
|
||||
for key, val in vars(subcmd_ns).items():
|
||||
if key == "subcommand":
|
||||
continue
|
||||
if isinstance(val, argparse.Namespace):
|
||||
continue # Skip nested namespace (already handled above)
|
||||
setattr(args, key, val)
|
||||
|
||||
# Ensure paths and patterns exist as lists (used as accumulation targets during parsing).
|
||||
# jsonargparse may set these to None rather than omitting them.
|
||||
if not getattr(args, "paths", None):
|
||||
args.paths = []
|
||||
if not getattr(args, "patterns", None):
|
||||
args.patterns = []
|
||||
|
||||
return args
|
||||
|
|
@ -1309,13 +1309,27 @@ class Highlander(argparse.Action):
|
|||
"""make sure some option is only given once"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# Pick up type function from class attribute (set by BoundHighlander in jap_wrapper.py)
|
||||
# or from kwargs (for direct use with standard argparse).
|
||||
self._type_fn = getattr(self.__class__, "_type_fn_override", None) or kwargs.pop("type", None)
|
||||
self.__called = False
|
||||
super().__init__(*args, **kwargs)
|
||||
# Apply the type function to string defaults so that converted values
|
||||
# are used even when the option is not given on the command line.
|
||||
# We need to do this ourselves because type= was stripped before reaching
|
||||
# argparse/jsonargparse, so they won't auto-convert the default.
|
||||
if self._type_fn is not None and isinstance(self.default, str):
|
||||
self.default = self._type_fn(self.default)
|
||||
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
if self.__called:
|
||||
raise argparse.ArgumentError(self, "There can be only one.")
|
||||
self.__called = True
|
||||
if self._type_fn is not None and isinstance(values, str):
|
||||
try:
|
||||
values = self._type_fn(values)
|
||||
except argparse.ArgumentTypeError as e:
|
||||
raise argparse.ArgumentError(self, str(e))
|
||||
setattr(namespace, self.dest, values)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,11 @@ class ArgparsePatternAction(argparse.Action):
|
|||
super().__init__(nargs=nargs, **kw)
|
||||
|
||||
def __call__(self, parser, args, values, option_string=None):
|
||||
# jsonargparse may initialize list-like attributes to None instead of []
|
||||
if args.paths is None:
|
||||
args.paths = []
|
||||
if args.patterns is None:
|
||||
args.patterns = []
|
||||
parse_patternfile_line(values[0], args.paths, args.patterns, ShellPattern)
|
||||
|
||||
|
||||
|
|
@ -60,11 +65,19 @@ class ArgparsePatternFileAction(argparse.Action):
|
|||
raise Error(str(e))
|
||||
|
||||
def parse(self, fobj, args):
|
||||
# jsonargparse may initialize list-like attributes to None instead of []
|
||||
if args.paths is None:
|
||||
args.paths = []
|
||||
if args.patterns is None:
|
||||
args.patterns = []
|
||||
load_pattern_file(fobj, args.paths, args.patterns)
|
||||
|
||||
|
||||
class ArgparseExcludeFileAction(ArgparsePatternFileAction):
|
||||
def parse(self, fobj, args):
|
||||
# jsonargparse may initialize list-like attributes to None instead of []
|
||||
if args.patterns is None:
|
||||
args.patterns = []
|
||||
load_exclude_file(fobj, args.patterns)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ def test_get_args():
|
|||
args = archiver.get_args(
|
||||
["borg", "serve", "--umask=0027", "--restrict-to-path=/p1", "--restrict-to-path=/p2"], "borg serve --info"
|
||||
)
|
||||
assert args.func == archiver.do_serve
|
||||
assert archiver.get_func(args) == archiver.do_serve
|
||||
assert args.restrict_to_paths == ["/p1", "/p2"]
|
||||
assert args.umask == 0o027
|
||||
assert args.log_level == "info"
|
||||
|
|
@ -66,13 +66,13 @@ def test_get_args():
|
|||
["borg", "serve", "--restrict-to-path=/p1", "--restrict-to-path=/p2"],
|
||||
f"borg --repo=/ repo-create {RK_ENCRYPTION}",
|
||||
)
|
||||
assert args.func == archiver.do_serve
|
||||
assert archiver.get_func(args) == archiver.do_serve
|
||||
|
||||
# Check that environment variables in the forced command don't cause issues. If the command
|
||||
# were not forced, environment variables would be interpreted by the shell, but this does not
|
||||
# happen for forced commands - we get the verbatim command line and need to deal with env vars.
|
||||
args = archiver.get_args(["borg", "serve"], "BORG_FOO=bar borg serve --info")
|
||||
assert args.func == archiver.do_serve
|
||||
assert archiver.get_func(args) == archiver.do_serve
|
||||
|
||||
|
||||
class TestCommonOptions:
|
||||
|
|
|
|||
Loading…
Reference in a new issue