mirror of
https://github.com/borgbackup/borg.git
synced 2025-01-01 04:37:34 +00:00
Merge pull request #1652 from ThomasWaldmann/merge-1.0-maint
Merge 1.0 maint
This commit is contained in:
commit
62bec8ab1c
12 changed files with 344 additions and 164 deletions
6
Vagrantfile
vendored
6
Vagrantfile
vendored
|
@ -60,10 +60,10 @@ def packages_darwin
|
|||
return <<-EOF
|
||||
# install all the (security and other) updates
|
||||
sudo softwareupdate --install --all
|
||||
# get osxfuse 3.x pre-release code from github:
|
||||
curl -s -L https://github.com/osxfuse/osxfuse/releases/download/osxfuse-3.4.1/osxfuse-3.4.1.dmg >osxfuse.dmg
|
||||
# get osxfuse 3.x release code from github:
|
||||
curl -s -L https://github.com/osxfuse/osxfuse/releases/download/osxfuse-3.5.2/osxfuse-3.5.2.dmg >osxfuse.dmg
|
||||
MOUNTDIR=$(echo `hdiutil mount osxfuse.dmg | tail -1 | awk '{$1="" ; print $0}'` | xargs -0 echo) \
|
||||
&& sudo installer -pkg "${MOUNTDIR}/Extras/FUSE for macOS 3.4.1.pkg" -target /
|
||||
&& sudo installer -pkg "${MOUNTDIR}/Extras/FUSE for macOS 3.5.2.pkg" -target /
|
||||
sudo chown -R vagrant /usr/local # brew must be able to create stuff here
|
||||
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
|
||||
brew update
|
||||
|
|
|
@ -158,7 +158,7 @@ The documentation (in reStructuredText format, .rst) is in docs/.
|
|||
|
||||
To build the html version of it, you need to have sphinx installed::
|
||||
|
||||
pip3 install sphinx # important: this will install sphinx with Python 3
|
||||
pip3 install sphinx sphinx_rtd_theme # important: this will install sphinx with Python 3
|
||||
|
||||
Now run::
|
||||
|
||||
|
|
|
@ -248,8 +248,13 @@ For automated backups the passphrase can be specified using the
|
|||
the key in case it gets corrupted or lost. Also keep your passphrase
|
||||
at a safe place.
|
||||
|
||||
The backup that is encrypted with that key/passphrase won't help you
|
||||
with that, of course.
|
||||
You can make backups using :ref:`borg_key_export` subcommand.
|
||||
|
||||
If you want to print a backup of your key to paper use the ``--paper``
|
||||
option of this command and print the result.
|
||||
|
||||
A backup inside of the backup that is encrypted with that key/passphrase
|
||||
won't help you with that, of course.
|
||||
|
||||
.. _remote_repos:
|
||||
|
||||
|
|
|
@ -538,6 +538,12 @@ borgfs
|
|||
standalone binary will have to manually create a symlink (see
|
||||
:ref:`pyinstaller-binary`).
|
||||
|
||||
.. include:: usage/key_export.rst.inc
|
||||
|
||||
|
||||
.. include:: usage/key_import.rst.inc
|
||||
|
||||
|
||||
.. include:: usage/change-passphrase.rst.inc
|
||||
|
||||
Examples
|
||||
|
|
37
setup.py
37
setup.py
|
@ -168,19 +168,33 @@ def finalize_options(self):
|
|||
|
||||
def run(self):
|
||||
print('generating usage docs')
|
||||
if not os.path.exists('docs/usage'):
|
||||
os.mkdir('docs/usage')
|
||||
# allows us to build docs without the C modules fully loaded during help generation
|
||||
from borg.archiver import Archiver
|
||||
parser = Archiver(prog='borg').parser
|
||||
|
||||
self.generate_level("", parser, Archiver)
|
||||
|
||||
def generate_level(self, prefix, parser, Archiver):
|
||||
is_subcommand = False
|
||||
choices = {}
|
||||
for action in parser._actions:
|
||||
if action.choices is not None:
|
||||
choices.update(action.choices)
|
||||
if action.choices is not None and 'SubParsersAction' in str(action.__class__):
|
||||
is_subcommand = True
|
||||
for cmd, parser in action.choices.items():
|
||||
choices[prefix + cmd] = parser
|
||||
if prefix and not choices:
|
||||
return
|
||||
print('found commands: %s' % list(choices.keys()))
|
||||
if not os.path.exists('docs/usage'):
|
||||
os.mkdir('docs/usage')
|
||||
|
||||
for command, parser in choices.items():
|
||||
print('generating help for %s' % command)
|
||||
with open('docs/usage/%s.rst.inc' % command, 'w') as doc:
|
||||
|
||||
if self.generate_level(command + " ", parser, Archiver):
|
||||
break
|
||||
|
||||
with open('docs/usage/%s.rst.inc' % command.replace(" ", "_"), 'w') as doc:
|
||||
doc.write(".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n")
|
||||
if command == 'help':
|
||||
for topic in Archiver.helptext:
|
||||
|
@ -191,8 +205,9 @@ def run(self):
|
|||
doc.write(Archiver.helptext[topic])
|
||||
else:
|
||||
params = {"command": command,
|
||||
"command_": command.replace(' ', '_'),
|
||||
"underline": '-' * len('borg ' + command)}
|
||||
doc.write(".. _borg_{command}:\n\n".format(**params))
|
||||
doc.write(".. _borg_{command_}:\n\n".format(**params))
|
||||
doc.write("borg {command}\n{underline}\n::\n\n borg {command}".format(**params))
|
||||
self.write_usage(parser, doc)
|
||||
epilog = parser.epilog
|
||||
|
@ -200,9 +215,13 @@ def run(self):
|
|||
self.write_options(parser, doc)
|
||||
doc.write("\n\nDescription\n~~~~~~~~~~~\n")
|
||||
doc.write(epilog)
|
||||
common_options = [group for group in choices['create']._action_groups if group.title == 'Common options'][0]
|
||||
with open('docs/usage/common-options.rst.inc', 'w') as doc:
|
||||
self.write_options_group(common_options, doc, False)
|
||||
|
||||
if 'create' in choices:
|
||||
common_options = [group for group in choices['create']._action_groups if group.title == 'Common options'][0]
|
||||
with open('docs/usage/common-options.rst.inc', 'w') as doc:
|
||||
self.write_options_group(common_options, doc, False)
|
||||
|
||||
return is_subcommand
|
||||
|
||||
def write_usage(self, parser, fp):
|
||||
if any(len(o.option_strings) for o in parser._actions):
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
from .helpers import dir_is_tagged, is_slow_msgpack, yes, sysinfo
|
||||
from .helpers import log_multi
|
||||
from .helpers import parse_pattern, PatternMatcher, PathPrefixPattern
|
||||
from .helpers import signal_handler
|
||||
from .helpers import signal_handler, raising_signal_handler, SigHup, SigTerm
|
||||
from .helpers import ErrorIgnoringTextIOWrapper
|
||||
from .helpers import ProgressIndicatorPercent
|
||||
from .item import Item
|
||||
|
@ -200,7 +200,8 @@ def do_check(self, args, repository):
|
|||
msg = ("'check --repair' is an experimental feature that might result in data loss." +
|
||||
"\n" +
|
||||
"Type 'YES' if you understand this and want to continue: ")
|
||||
if not yes(msg, false_msg="Aborting.", truish=('YES', ),
|
||||
if not yes(msg, false_msg="Aborting.", invalid_msg="Invalid answer, aborting.",
|
||||
truish=('YES', ), retry=False,
|
||||
env_var_override='BORG_CHECK_I_KNOW_WHAT_I_AM_DOING'):
|
||||
return EXIT_ERROR
|
||||
if args.repo_only and args.verify_data:
|
||||
|
@ -798,8 +799,8 @@ def do_delete(self, args, repository):
|
|||
msg.append(format_archive(archive_info))
|
||||
msg.append("Type 'YES' if you understand this and want to continue: ")
|
||||
msg = '\n'.join(msg)
|
||||
if not yes(msg, false_msg="Aborting.", truish=('YES', ),
|
||||
env_var_override='BORG_DELETE_I_KNOW_WHAT_I_AM_DOING'):
|
||||
if not yes(msg, false_msg="Aborting.", invalid_msg='Invalid answer, aborting.', truish=('YES', ),
|
||||
retry=False, env_var_override='BORG_DELETE_I_KNOW_WHAT_I_AM_DOING'):
|
||||
self.exit_code = EXIT_ERROR
|
||||
return self.exit_code
|
||||
repository.destroy()
|
||||
|
@ -1621,10 +1622,37 @@ def build_parser(self, prog=None):
|
|||
subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='',
|
||||
type=location_validator(archive=False))
|
||||
|
||||
subparser = subparsers.add_parser('key-export', parents=[common_parser], add_help=False,
|
||||
description=self.do_key_export.__doc__,
|
||||
subparser = subparsers.add_parser('key', add_help=False,
|
||||
description="Manage a keyfile or repokey of a repository",
|
||||
epilog="",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help='manage repository key')
|
||||
|
||||
key_parsers = subparser.add_subparsers(title='required arguments', metavar='<command>')
|
||||
|
||||
key_export_epilog = textwrap.dedent("""
|
||||
If repository encryption is used, the repository is inaccessible
|
||||
without the key. This command allows to backup this essential key.
|
||||
|
||||
There are two backup formats. The normal backup format is suitable for
|
||||
digital storage as a file. The ``--paper`` backup format is optimized
|
||||
for printing and typing in while importing, with per line checks to
|
||||
reduce problems with manual input.
|
||||
|
||||
For repositories using keyfile encryption the key is saved locally
|
||||
on the system that is capable of doing backups. To guard against loss
|
||||
of this key, the key needs to be backed up independently of the main
|
||||
data backup.
|
||||
|
||||
For repositories using the repokey encryption the key is saved in the
|
||||
repository in the config file. A backup is thus not strictly needed,
|
||||
but guards against the repository becoming inaccessible if the file
|
||||
is damaged for some reason.
|
||||
""")
|
||||
subparser = key_parsers.add_parser('export', parents=[common_parser], add_help=False,
|
||||
description=self.do_key_export.__doc__,
|
||||
epilog=key_export_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help='export repository key for backup')
|
||||
subparser.set_defaults(func=self.do_key_export)
|
||||
subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='',
|
||||
|
@ -1635,9 +1663,17 @@ def build_parser(self, prog=None):
|
|||
default=False,
|
||||
help='Create an export suitable for printing and later type-in')
|
||||
|
||||
subparser = subparsers.add_parser('key-import', parents=[common_parser], add_help=False,
|
||||
key_import_epilog = textwrap.dedent("""
|
||||
This command allows to restore a key previously backed up with the
|
||||
export command.
|
||||
|
||||
If the ``--paper`` option is given, the import will be an interactive
|
||||
process in which each line is checked for plausibility before
|
||||
proceeding to the next line. For this format PATH must not be given.
|
||||
""")
|
||||
subparser = key_parsers.add_parser('import', parents=[common_parser], add_help=False,
|
||||
description=self.do_key_import.__doc__,
|
||||
epilog="",
|
||||
epilog=key_import_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help='import repository key from backup')
|
||||
subparser.set_defaults(func=self.do_key_import)
|
||||
|
@ -2345,6 +2381,22 @@ def build_parser(self, prog=None):
|
|||
subparser.add_argument('topic', metavar='TOPIC', type=str, nargs='?',
|
||||
help='additional help on TOPIC')
|
||||
|
||||
debug_epilog = textwrap.dedent("""
|
||||
These commands are not intended for normal use and potentially very
|
||||
dangerous if used incorrectly.
|
||||
|
||||
They exist to improve debugging capabilities without direct system access, e.g.
|
||||
in case you ever run into some severe malfunction. Use them only if you know
|
||||
what you are doing or if a trusted developer tells you what to do.""")
|
||||
|
||||
subparser = subparsers.add_parser('debug', 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)')
|
||||
|
||||
debug_parsers = subparser.add_subparsers(title='required arguments', metavar='<command>')
|
||||
|
||||
debug_info_epilog = textwrap.dedent("""
|
||||
This command displays some system information that might be useful for bug
|
||||
reports and debugging problems. If a traceback happens, this information is
|
||||
|
@ -2357,6 +2409,13 @@ def build_parser(self, prog=None):
|
|||
help='show system infos for debugging / bug reports (debug)')
|
||||
subparser.set_defaults(func=self.do_debug_info)
|
||||
|
||||
subparser = debug_parsers.add_parser('info', 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_dump_archive_items_epilog = textwrap.dedent("""
|
||||
This command dumps raw (but decrypted and decompressed) archive items (only metadata) to files.
|
||||
""")
|
||||
|
@ -2370,6 +2429,16 @@ def build_parser(self, prog=None):
|
|||
type=location_validator(archive=True),
|
||||
help='archive to dump')
|
||||
|
||||
subparser = debug_parsers.add_parser('dump-archive-items', 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)
|
||||
subparser.add_argument('location', metavar='ARCHIVE',
|
||||
type=location_validator(archive=True),
|
||||
help='archive to dump')
|
||||
|
||||
debug_dump_repo_objs_epilog = textwrap.dedent("""
|
||||
This command dumps raw (but decrypted and decompressed) repo objects to files.
|
||||
""")
|
||||
|
@ -2383,6 +2452,16 @@ def build_parser(self, prog=None):
|
|||
type=location_validator(archive=False),
|
||||
help='repo to dump')
|
||||
|
||||
subparser = debug_parsers.add_parser('dump-repo-objs', 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)
|
||||
subparser.add_argument('location', metavar='REPOSITORY',
|
||||
type=location_validator(archive=False),
|
||||
help='repo to dump')
|
||||
|
||||
debug_get_obj_epilog = textwrap.dedent("""
|
||||
This command gets an object from the repository.
|
||||
""")
|
||||
|
@ -2400,6 +2479,20 @@ def build_parser(self, prog=None):
|
|||
subparser.add_argument('path', metavar='PATH', type=str,
|
||||
help='file to write object data into')
|
||||
|
||||
subparser = debug_parsers.add_parser('get-obj', 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)
|
||||
subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='',
|
||||
type=location_validator(archive=False),
|
||||
help='repository to use')
|
||||
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')
|
||||
|
||||
debug_put_obj_epilog = textwrap.dedent("""
|
||||
This command puts objects into the repository.
|
||||
""")
|
||||
|
@ -2415,6 +2508,18 @@ def build_parser(self, prog=None):
|
|||
subparser.add_argument('paths', metavar='PATH', nargs='+', type=str,
|
||||
help='file(s) to read and create object(s) from')
|
||||
|
||||
subparser = debug_parsers.add_parser('put-obj', 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)
|
||||
subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='',
|
||||
type=location_validator(archive=False),
|
||||
help='repository to use')
|
||||
subparser.add_argument('paths', metavar='PATH', nargs='+', type=str,
|
||||
help='file(s) to read and create object(s) from')
|
||||
|
||||
debug_delete_obj_epilog = textwrap.dedent("""
|
||||
This command deletes objects from the repository.
|
||||
""")
|
||||
|
@ -2429,6 +2534,19 @@ def build_parser(self, prog=None):
|
|||
help='repository to use')
|
||||
subparser.add_argument('ids', metavar='IDs', nargs='+', type=str,
|
||||
help='hex object ID(s) to delete from the repo')
|
||||
|
||||
subparser = debug_parsers.add_parser('delete-obj', 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)
|
||||
subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='',
|
||||
type=location_validator(archive=False),
|
||||
help='repository to use')
|
||||
subparser.add_argument('ids', metavar='IDs', nargs='+', type=str,
|
||||
help='hex object ID(s) to delete from the repo')
|
||||
|
||||
return parser
|
||||
|
||||
def get_args(self, argv, cmd):
|
||||
|
@ -2494,59 +2612,28 @@ def run(self, args):
|
|||
return args.func(args)
|
||||
|
||||
|
||||
def sig_info_handler(signum, stack): # pragma: no cover
|
||||
def sig_info_handler(sig_no, stack): # pragma: no cover
|
||||
"""search the stack for infos about the currently processed file and print them"""
|
||||
for frame in inspect.getouterframes(stack):
|
||||
func, loc = frame[3], frame[0].f_locals
|
||||
if func in ('process_file', '_process', ): # create op
|
||||
path = loc['path']
|
||||
try:
|
||||
pos = loc['fd'].tell()
|
||||
total = loc['st'].st_size
|
||||
except Exception:
|
||||
pos, total = 0, 0
|
||||
logger.info("{0} {1}/{2}".format(path, format_file_size(pos), format_file_size(total)))
|
||||
break
|
||||
if func in ('extract_item', ): # extract op
|
||||
path = loc['item'].path
|
||||
try:
|
||||
pos = loc['fd'].tell()
|
||||
except Exception:
|
||||
pos = 0
|
||||
logger.info("{0} {1}/???".format(path, format_file_size(pos)))
|
||||
break
|
||||
|
||||
|
||||
class SIGTERMReceived(BaseException):
|
||||
pass
|
||||
|
||||
|
||||
def sig_term_handler(signum, stack):
|
||||
raise SIGTERMReceived
|
||||
|
||||
|
||||
class SIGHUPReceived(BaseException):
|
||||
pass
|
||||
|
||||
|
||||
def sig_hup_handler(signum, stack):
|
||||
raise SIGHUPReceived
|
||||
|
||||
|
||||
def setup_signal_handlers(): # pragma: no cover
|
||||
sigs = []
|
||||
if hasattr(signal, 'SIGUSR1'):
|
||||
sigs.append(signal.SIGUSR1) # kill -USR1 pid
|
||||
if hasattr(signal, 'SIGINFO'):
|
||||
sigs.append(signal.SIGINFO) # kill -INFO pid (or ctrl-t)
|
||||
for sig in sigs:
|
||||
signal.signal(sig, sig_info_handler)
|
||||
# If we received SIGTERM or SIGHUP, catch them and raise a proper exception
|
||||
# that can be handled for an orderly exit. SIGHUP is important especially
|
||||
# for systemd systems, where logind sends it when a session exits, in
|
||||
# addition to any traditional use.
|
||||
signal.signal(signal.SIGTERM, sig_term_handler)
|
||||
signal.signal(signal.SIGHUP, sig_hup_handler)
|
||||
with signal_handler(sig_no, signal.SIG_IGN):
|
||||
for frame in inspect.getouterframes(stack):
|
||||
func, loc = frame[3], frame[0].f_locals
|
||||
if func in ('process_file', '_process', ): # create op
|
||||
path = loc['path']
|
||||
try:
|
||||
pos = loc['fd'].tell()
|
||||
total = loc['st'].st_size
|
||||
except Exception:
|
||||
pos, total = 0, 0
|
||||
logger.info("{0} {1}/{2}".format(path, format_file_size(pos), format_file_size(total)))
|
||||
break
|
||||
if func in ('extract_item', ): # extract op
|
||||
path = loc['item'].path
|
||||
try:
|
||||
pos = loc['fd'].tell()
|
||||
except Exception:
|
||||
pos = 0
|
||||
logger.info("{0} {1}/???".format(path, format_file_size(pos)))
|
||||
break
|
||||
|
||||
|
||||
def main(): # pragma: no cover
|
||||
|
@ -2558,68 +2645,79 @@ def main(): # pragma: no cover
|
|||
# issues when print()-ing unicode file names
|
||||
sys.stdout = ErrorIgnoringTextIOWrapper(sys.stdout.buffer, sys.stdout.encoding, 'replace', line_buffering=True)
|
||||
sys.stderr = ErrorIgnoringTextIOWrapper(sys.stderr.buffer, sys.stderr.encoding, 'replace', line_buffering=True)
|
||||
setup_signal_handlers()
|
||||
archiver = Archiver()
|
||||
msg = tb = None
|
||||
tb_log_level = logging.ERROR
|
||||
try:
|
||||
args = archiver.get_args(sys.argv, os.environ.get('SSH_ORIGINAL_COMMAND'))
|
||||
except Error as e:
|
||||
msg = e.get_message()
|
||||
tb_log_level = logging.ERROR if e.traceback else logging.DEBUG
|
||||
tb = '%s\n%s' % (traceback.format_exc(), sysinfo())
|
||||
# we might not have logging setup yet, so get out quickly
|
||||
print(msg, file=sys.stderr)
|
||||
if tb_log_level == logging.ERROR:
|
||||
print(tb, file=sys.stderr)
|
||||
sys.exit(e.exit_code)
|
||||
try:
|
||||
exit_code = archiver.run(args)
|
||||
except Error as e:
|
||||
msg = e.get_message()
|
||||
tb_log_level = logging.ERROR if e.traceback else logging.DEBUG
|
||||
tb = "%s\n%s" % (traceback.format_exc(), sysinfo())
|
||||
exit_code = e.exit_code
|
||||
except RemoteRepository.RPCError as e:
|
||||
msg = "%s %s" % (e.remote_type, e.name)
|
||||
important = e.remote_type not in ('LockTimeout', )
|
||||
tb_log_level = logging.ERROR if important else logging.DEBUG
|
||||
tb = sysinfo()
|
||||
exit_code = EXIT_ERROR
|
||||
except Exception:
|
||||
msg = 'Local Exception'
|
||||
|
||||
# If we receive SIGINT (ctrl-c), SIGTERM (kill) or SIGHUP (kill -HUP),
|
||||
# catch them and raise a proper exception that can be handled for an
|
||||
# orderly exit.
|
||||
# SIGHUP is important especially for systemd systems, where logind
|
||||
# sends it when a session exits, in addition to any traditional use.
|
||||
# Output some info if we receive SIGUSR1 or SIGINFO (ctrl-t).
|
||||
with signal_handler('SIGINT', raising_signal_handler(KeyboardInterrupt)), \
|
||||
signal_handler('SIGHUP', raising_signal_handler(SigHup)), \
|
||||
signal_handler('SIGTERM', raising_signal_handler(SigTerm)), \
|
||||
signal_handler('SIGUSR1', sig_info_handler), \
|
||||
signal_handler('SIGINFO', sig_info_handler):
|
||||
archiver = Archiver()
|
||||
msg = tb = None
|
||||
tb_log_level = logging.ERROR
|
||||
tb = '%s\n%s' % (traceback.format_exc(), sysinfo())
|
||||
exit_code = EXIT_ERROR
|
||||
except KeyboardInterrupt:
|
||||
msg = 'Keyboard interrupt'
|
||||
tb_log_level = logging.DEBUG
|
||||
tb = '%s\n%s' % (traceback.format_exc(), sysinfo())
|
||||
exit_code = EXIT_ERROR
|
||||
except SIGTERMReceived:
|
||||
msg = 'Received SIGTERM'
|
||||
tb_log_level = logging.DEBUG
|
||||
tb = '%s\n%s' % (traceback.format_exc(), sysinfo())
|
||||
exit_code = EXIT_ERROR
|
||||
except SIGHUPReceived:
|
||||
msg = 'Received SIGHUP.'
|
||||
exit_code = EXIT_ERROR
|
||||
if msg:
|
||||
logger.error(msg)
|
||||
if tb:
|
||||
logger.log(tb_log_level, tb)
|
||||
if args.show_rc:
|
||||
rc_logger = logging.getLogger('borg.output.show-rc')
|
||||
exit_msg = 'terminating with %s status, rc %d'
|
||||
if exit_code == EXIT_SUCCESS:
|
||||
rc_logger.info(exit_msg % ('success', exit_code))
|
||||
elif exit_code == EXIT_WARNING:
|
||||
rc_logger.warning(exit_msg % ('warning', exit_code))
|
||||
elif exit_code == EXIT_ERROR:
|
||||
rc_logger.error(exit_msg % ('error', exit_code))
|
||||
else:
|
||||
rc_logger.error(exit_msg % ('abnormal', exit_code or 666))
|
||||
sys.exit(exit_code)
|
||||
try:
|
||||
args = archiver.get_args(sys.argv, os.environ.get('SSH_ORIGINAL_COMMAND'))
|
||||
except Error as e:
|
||||
msg = e.get_message()
|
||||
tb_log_level = logging.ERROR if e.traceback else logging.DEBUG
|
||||
tb = '%s\n%s' % (traceback.format_exc(), sysinfo())
|
||||
# we might not have logging setup yet, so get out quickly
|
||||
print(msg, file=sys.stderr)
|
||||
if tb_log_level == logging.ERROR:
|
||||
print(tb, file=sys.stderr)
|
||||
sys.exit(e.exit_code)
|
||||
try:
|
||||
exit_code = archiver.run(args)
|
||||
except Error as e:
|
||||
msg = e.get_message()
|
||||
tb_log_level = logging.ERROR if e.traceback else logging.DEBUG
|
||||
tb = "%s\n%s" % (traceback.format_exc(), sysinfo())
|
||||
exit_code = e.exit_code
|
||||
except RemoteRepository.RPCError as e:
|
||||
msg = "%s %s" % (e.remote_type, e.name)
|
||||
important = e.remote_type not in ('LockTimeout', )
|
||||
tb_log_level = logging.ERROR if important else logging.DEBUG
|
||||
tb = sysinfo()
|
||||
exit_code = EXIT_ERROR
|
||||
except Exception:
|
||||
msg = 'Local Exception'
|
||||
tb_log_level = logging.ERROR
|
||||
tb = '%s\n%s' % (traceback.format_exc(), sysinfo())
|
||||
exit_code = EXIT_ERROR
|
||||
except KeyboardInterrupt:
|
||||
msg = 'Keyboard interrupt'
|
||||
tb_log_level = logging.DEBUG
|
||||
tb = '%s\n%s' % (traceback.format_exc(), sysinfo())
|
||||
exit_code = EXIT_ERROR
|
||||
except SigTerm:
|
||||
msg = 'Received SIGTERM'
|
||||
tb_log_level = logging.DEBUG
|
||||
tb = '%s\n%s' % (traceback.format_exc(), sysinfo())
|
||||
exit_code = EXIT_ERROR
|
||||
except SigHup:
|
||||
msg = 'Received SIGHUP.'
|
||||
exit_code = EXIT_ERROR
|
||||
if msg:
|
||||
logger.error(msg)
|
||||
if tb:
|
||||
logger.log(tb_log_level, tb)
|
||||
if args.show_rc:
|
||||
rc_logger = logging.getLogger('borg.output.show-rc')
|
||||
exit_msg = 'terminating with %s status, rc %d'
|
||||
if exit_code == EXIT_SUCCESS:
|
||||
rc_logger.info(exit_msg % ('success', exit_code))
|
||||
elif exit_code == EXIT_WARNING:
|
||||
rc_logger.warning(exit_msg % ('warning', exit_code))
|
||||
elif exit_code == EXIT_ERROR:
|
||||
rc_logger.error(exit_msg % ('error', exit_code))
|
||||
else:
|
||||
rc_logger.error(exit_msg % ('abnormal', exit_code or 666))
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -78,7 +78,8 @@ def __init__(self, repository, key, manifest, path=None, sync=True, do_files=Fal
|
|||
msg = ("Warning: Attempting to access a previously unknown unencrypted repository!" +
|
||||
"\n" +
|
||||
"Do you want to continue? [yN] ")
|
||||
if not yes(msg, false_msg="Aborting.", env_var_override='BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK'):
|
||||
if not yes(msg, false_msg="Aborting.", invalid_msg="Invalid answer, aborting.",
|
||||
retry=False, env_var_override='BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK'):
|
||||
raise self.CacheInitAbortedError()
|
||||
self.create()
|
||||
self.open(lock_wait=lock_wait)
|
||||
|
@ -88,7 +89,8 @@ def __init__(self, repository, key, manifest, path=None, sync=True, do_files=Fal
|
|||
msg = ("Warning: The repository at location {} was previously located at {}".format(repository._location.canonical_path(), self.previous_location) +
|
||||
"\n" +
|
||||
"Do you want to continue? [yN] ")
|
||||
if not yes(msg, false_msg="Aborting.", env_var_override='BORG_RELOCATED_REPO_ACCESS_IS_OK'):
|
||||
if not yes(msg, false_msg="Aborting.", invalid_msg="Invalid answer, aborting.",
|
||||
retry=False, env_var_override='BORG_RELOCATED_REPO_ACCESS_IS_OK'):
|
||||
raise self.RepositoryAccessAborted()
|
||||
|
||||
if sync and self.manifest.id != self.manifest_id:
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import argparse
|
||||
import contextlib
|
||||
import grp
|
||||
import hashlib
|
||||
import logging
|
||||
|
@ -19,7 +20,6 @@
|
|||
import uuid
|
||||
from binascii import hexlify
|
||||
from collections import namedtuple, deque, abc
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from fnmatch import translate
|
||||
from functools import wraps, partial, lru_cache
|
||||
|
@ -1054,9 +1054,8 @@ def yes(msg=None, false_msg=None, true_msg=None, default_msg=None,
|
|||
default=False, retry=True, env_var_override=None, ofile=None, input=input):
|
||||
"""Output <msg> (usually a question) and let user input an answer.
|
||||
Qualifies the answer according to falsish, truish and defaultish as True, False or <default>.
|
||||
If it didn't qualify and retry_msg is None (no retries wanted),
|
||||
return the default [which defaults to False]. Otherwise let user retry
|
||||
answering until answer is qualified.
|
||||
If it didn't qualify and retry is False (no retries wanted), return the default [which
|
||||
defaults to False]. If retry is True let user retry answering until answer is qualified.
|
||||
|
||||
If env_var_override is given and this var is present in the environment, do not ask
|
||||
the user, but just use the env var contents as answer as if it was typed in.
|
||||
|
@ -1665,15 +1664,6 @@ def heuristic_lz4(self, compr_args, chunk):
|
|||
return compr_args, Chunk(data, **meta)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def signal_handler(signo, handler):
|
||||
old_signal_handler = signal.signal(signo, handler)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
signal.signal(signo, old_signal_handler)
|
||||
|
||||
|
||||
class ErrorIgnoringTextIOWrapper(io.TextIOWrapper):
|
||||
def read(self, n):
|
||||
if not self.closed:
|
||||
|
@ -1698,6 +1688,52 @@ def write(self, s):
|
|||
return len(s)
|
||||
|
||||
|
||||
class SignalException(BaseException):
|
||||
"""base class for all signal-based exceptions"""
|
||||
|
||||
|
||||
class SigHup(SignalException):
|
||||
"""raised on SIGHUP signal"""
|
||||
|
||||
|
||||
class SigTerm(SignalException):
|
||||
"""raised on SIGTERM signal"""
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def signal_handler(sig, handler):
|
||||
"""
|
||||
when entering context, set up signal handler <handler> for signal <sig>.
|
||||
when leaving context, restore original signal handler.
|
||||
|
||||
<sig> can bei either a str when giving a signal.SIGXXX attribute name (it
|
||||
won't crash if the attribute name does not exist as some names are platform
|
||||
specific) or a int, when giving a signal number.
|
||||
|
||||
<handler> is any handler value as accepted by the signal.signal(sig, handler).
|
||||
"""
|
||||
if isinstance(sig, str):
|
||||
sig = getattr(signal, sig, None)
|
||||
if sig is not None:
|
||||
orig_handler = signal.signal(sig, handler)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if sig is not None:
|
||||
signal.signal(sig, orig_handler)
|
||||
|
||||
|
||||
def raising_signal_handler(exc_cls):
|
||||
def handler(sig_no, frame):
|
||||
# setting SIG_IGN avoids that an incoming second signal of this
|
||||
# kind would raise a 2nd exception while we still process the
|
||||
# exception handler for exc_cls for the 1st signal.
|
||||
signal.signal(sig_no, signal.SIG_IGN)
|
||||
raise exc_cls
|
||||
|
||||
return handler
|
||||
|
||||
|
||||
def swidth_slice(string, max_width):
|
||||
"""
|
||||
Return a slice of *max_width* cells from *string*.
|
||||
|
|
|
@ -228,8 +228,9 @@ def getpass(cls, prompt):
|
|||
|
||||
@classmethod
|
||||
def verification(cls, passphrase):
|
||||
if yes('Do you want your passphrase to be displayed for verification? [yN]: ',
|
||||
env_var_override='BORG_DISPLAY_PASSPHRASE'):
|
||||
msg = 'Do you want your passphrase to be displayed for verification? [yN]: '
|
||||
if yes(msg, retry_msg=msg, invalid_msg='Invalid answer, try again.',
|
||||
retry=True, env_var_override='BORG_DISPLAY_PASSPHRASE'):
|
||||
print('Your passphrase (between double-quotes): "%s"' % passphrase,
|
||||
file=sys.stderr)
|
||||
print('Make sure the passphrase displayed above is exactly what you wanted.',
|
||||
|
|
|
@ -98,7 +98,7 @@ def grouped(s):
|
|||
i += 1
|
||||
return ret
|
||||
|
||||
export = 'To restore key use borg key-import --paper /path/to/repo\n\n'
|
||||
export = 'To restore key use borg key import --paper /path/to/repo\n\n'
|
||||
|
||||
binary = a2b_base64(self.keyblob)
|
||||
export += 'BORG PAPER KEY v1\n'
|
||||
|
|
|
@ -980,6 +980,13 @@ def iter_objects(self, segment, include_data=False, read_data=True):
|
|||
else:
|
||||
yield tag, key, offset, size
|
||||
offset += size
|
||||
# we must get the fd via get_fd() here again as we yielded to our caller and it might
|
||||
# have triggered closing of the fd we had before (e.g. by calling io.read() for
|
||||
# different segment(s)).
|
||||
# by calling get_fd() here again we also make our fd "recently used" so it likely
|
||||
# does not get kicked out of self.fds LRUcache.
|
||||
fd = self.get_fd(segment)
|
||||
fd.seek(offset)
|
||||
header = fd.read(self.header_fmt.size)
|
||||
|
||||
def recover_segment(self, segment, filename):
|
||||
|
|
|
@ -76,7 +76,7 @@ def exec_cmd(*args, archiver=None, fork=False, exe=None, **kw):
|
|||
sys.stdin, sys.stdout, sys.stderr = stdin, stdout, stderr
|
||||
|
||||
|
||||
# check if the binary "borg.exe" is available
|
||||
# check if the binary "borg.exe" is available (for local testing a symlink to virtualenv/bin/borg should do)
|
||||
try:
|
||||
exec_cmd('help', exe='borg.exe', fork=True)
|
||||
BORG_EXES = ['python', 'binary', ]
|
||||
|
@ -1815,7 +1815,7 @@ def test_key_export_keyfile(self):
|
|||
export_file = self.output_path + '/exported'
|
||||
self.cmd('init', self.repository_location, '--encryption', 'keyfile')
|
||||
repo_id = self._extract_repository_id(self.repository_path)
|
||||
self.cmd('key-export', self.repository_location, export_file)
|
||||
self.cmd('key', 'export', self.repository_location, export_file)
|
||||
|
||||
with open(export_file, 'r') as fd:
|
||||
export_contents = fd.read()
|
||||
|
@ -1831,7 +1831,7 @@ def test_key_export_keyfile(self):
|
|||
|
||||
os.unlink(key_file)
|
||||
|
||||
self.cmd('key-import', self.repository_location, export_file)
|
||||
self.cmd('key', 'import', self.repository_location, export_file)
|
||||
|
||||
with open(key_file, 'r') as fd:
|
||||
key_contents2 = fd.read()
|
||||
|
@ -1842,7 +1842,7 @@ def test_key_export_repokey(self):
|
|||
export_file = self.output_path + '/exported'
|
||||
self.cmd('init', self.repository_location, '--encryption', 'repokey')
|
||||
repo_id = self._extract_repository_id(self.repository_path)
|
||||
self.cmd('key-export', self.repository_location, export_file)
|
||||
self.cmd('key', 'export', self.repository_location, export_file)
|
||||
|
||||
with open(export_file, 'r') as fd:
|
||||
export_contents = fd.read()
|
||||
|
@ -1861,7 +1861,7 @@ def test_key_export_repokey(self):
|
|||
with Repository(self.repository_path) as repository:
|
||||
repository.save_key(b'')
|
||||
|
||||
self.cmd('key-import', self.repository_location, export_file)
|
||||
self.cmd('key', 'import', self.repository_location, export_file)
|
||||
|
||||
with Repository(self.repository_path) as repository:
|
||||
repo_key2 = RepoKey(repository)
|
||||
|
@ -1873,17 +1873,23 @@ def test_key_import_errors(self):
|
|||
export_file = self.output_path + '/exported'
|
||||
self.cmd('init', self.repository_location, '--encryption', 'keyfile')
|
||||
|
||||
self.cmd('key-import', self.repository_location, export_file, exit_code=EXIT_ERROR)
|
||||
self.cmd('key', 'import', self.repository_location, export_file, exit_code=EXIT_ERROR)
|
||||
|
||||
with open(export_file, 'w') as fd:
|
||||
fd.write('something not a key\n')
|
||||
|
||||
self.assert_raises(NotABorgKeyFile, lambda: self.cmd('key-import', self.repository_location, export_file))
|
||||
if self.FORK_DEFAULT:
|
||||
self.cmd('key', 'import', self.repository_location, export_file, exit_code=2)
|
||||
else:
|
||||
self.assert_raises(NotABorgKeyFile, lambda: self.cmd('key', 'import', self.repository_location, export_file))
|
||||
|
||||
with open(export_file, 'w') as fd:
|
||||
fd.write('BORG_KEY a0a0a0\n')
|
||||
|
||||
self.assert_raises(RepoIdMismatch, lambda: self.cmd('key-import', self.repository_location, export_file))
|
||||
if self.FORK_DEFAULT:
|
||||
self.cmd('key', 'import', self.repository_location, export_file, exit_code=2)
|
||||
else:
|
||||
self.assert_raises(RepoIdMismatch, lambda: self.cmd('key', 'import', self.repository_location, export_file))
|
||||
|
||||
def test_key_export_paperkey(self):
|
||||
repo_id = 'e294423506da4e1ea76e8dcdf1a3919624ae3ae496fddf905610c351d3f09239'
|
||||
|
@ -1898,12 +1904,12 @@ def test_key_export_paperkey(self):
|
|||
fd.write(KeyfileKey.FILE_ID + ' ' + repo_id + '\n')
|
||||
fd.write(b2a_base64(b'abcdefghijklmnopqrstu').decode())
|
||||
|
||||
self.cmd('key-export', '--paper', self.repository_location, export_file)
|
||||
self.cmd('key', 'export', '--paper', self.repository_location, export_file)
|
||||
|
||||
with open(export_file, 'r') as fd:
|
||||
export_contents = fd.read()
|
||||
|
||||
assert export_contents == """To restore key use borg key-import --paper /path/to/repo
|
||||
assert export_contents == """To restore key use borg key import --paper /path/to/repo
|
||||
|
||||
BORG PAPER KEY v1
|
||||
id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02
|
||||
|
|
Loading…
Reference in a new issue