%PDF- %PDF-
Direktori : /lib/python3/dist-packages/duplicity/ |
Current File : //lib/python3/dist-packages/duplicity/cli_util.py |
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4; encoding:utf-8 -*- # # Copyright 2022 Kenneth Loafman <kenneth@loafman.com> # # This file is part of duplicity. # # Duplicity is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the # Free Software Foundation; either version 2 of the License, or (at your # option) any later version. # # Duplicity is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with duplicity; if not, write to the Free Software Foundation, # Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ Utils for parse command line, check for consistency, and set config """ import io import os import re import socket import sys from hashlib import md5 # TODO: Remove duplicity.argparse311 when py38 goes EOL if sys.version_info[0:2] == (3, 8): from duplicity import argparse311 as argparse else: import argparse from duplicity import config from duplicity import dup_time from duplicity import errors from duplicity import log from duplicity import path from duplicity import selection gpg_key_patt = re.compile(r"^(0x)?([0-9A-Fa-f]{8}|[0-9A-Fa-f]{16}|[0-9A-Fa-f]{40})$") url_regexp = re.compile(r"^[\w\+]+://") help_footer = _("Enter 'duplicity --help' for help screen.") class CommandLineError(errors.UserError): sys.tracebacklimit = 4 pass def command_line_error(message): """ Indicate a command line error and exit """ raise CommandLineError(f"{message}\n{help_footer}") class DuplicityAction(argparse.Action): def __init__(self, option_strings, dest, **kwargs): super().__init__(option_strings, dest, **kwargs) def __call__(self, parser, namespace, values, option_string=None): raise NotImplementedError class DoNothingAction(DuplicityAction): def __call__(self, parser, *args, **kw): pass class AddSelectionAction(DuplicityAction): def __init__(self, option_strings, dest, **kwargs): super().__init__(option_strings, dest, **kwargs) def __call__(self, parser, namespace, values, option_string=None): addarg = os.fsdecode(value) if isinstance(values, bytes) else values config.select_opts.append((os.fsdecode(option_string), addarg)) class AddFilelistAction(DuplicityAction): def __init__(self, option_strings, dest, **kwargs): super().__init__(option_strings, dest, **kwargs) def __call__(self, parser, namespace, values, option_string=None): config.select_opts.append((os.fsdecode(option_string), os.fsdecode(values))) try: config.select_files.append(io.open(values, "rt", encoding="UTF-8")) except Exception as e: command_line_error(str(e)) class AddRenameAction(DuplicityAction): def __init__(self, option_strings, dest, **kwargs): super().__init__(option_strings, dest, **kwargs) def __call__(self, parser, namespace, values, option_string=None): key = os.fsencode(os.path.normcase(os.path.normpath(values[0]))) config.rename[key] = os.fsencode(values[1]) class SplitOptionsAction(DuplicityAction): def __init__(self, option_strings, dest, **kwargs): super().__init__(option_strings, dest, **kwargs) def __call__(self, parser, namespace, values, option_string=None): var = opt2var(option_string) opts = getattr(namespace, var) values = values.strip('"').strip("'") if opts == "": opts = values else: opts = f"{opts} {values}" setattr(namespace, var, opts) class IgnoreErrorsAction(DuplicityAction): def __init__(self, option_strings, dest, **kwargs): super().__init__(option_strings, dest, **kwargs) def __call__(self, parser, namespace, values, option_string=None): log.Warn( _("Running in 'ignore errors' mode due to --ignore-errors.\n" "Please reconsider if this was not intended") ) config.ignore_errors = True class WarnAsyncStoreConstAction(argparse._StoreConstAction): def __init__(self, option_strings, dest, nargs=None, **kwargs): super().__init__(option_strings, dest, **kwargs) def __call__(self, parser, namespace, values, option_string=None): log.Warn( _( "Use of the --asynchronous-upload option is experimental " "and not safe for production! There are reported cases of " "undetected data loss during upload. Be aware and " "periodically verify your backups to be safe." ) ) setattr(namespace, self.dest, self.const) def _check_int(val): try: return int(val) except Exception as e: command_line_error(_(f"'{val}' is not an int: {str(e)}")) def _check_time(val): try: return dup_time.genstrtotime(val) except dup_time.TimeException as e: command_line_error(str(e)) def check_char(val): if len(val) == 1: return val else: command_line_error(_(f"'{val} is not a single character.")) def check_count(val): return _check_int(val) def check_file(val): try: return os.fsencode(expand_fn(val)) except Exception as e: command_line_error(f"{val} is not a valide pathname: {str(e)}") def check_interval(val): try: return dup_time.intstringtoseconds(val) except dup_time.TimeException as e: command_line_error(str(e)) def check_remove_time(val): return _check_time(val) def check_source_path(val): if not is_path(val): command_line_error(_(f"Source should be pathname, not url. Got '{val}' instead.")) if not os.path.exists(val): command_line_error(_(f"Argument source_path '{val}' does not exist.")) return val def check_source_url(val): if not is_url(val): command_line_error(_(f"Source should be url, not directory. Got '{val}' instead.")) return val def check_target_dir(val): if not is_path(val): command_line_error(_(f"Target should be directory, not url. Got '{val}' instead.")) if not os.path.exists(val): try: os.makedirs(val, exist_ok=True) except Exception as e: command_line_error(_(f"Unable to create target dir '{val}': {str(e)}")) return val def check_target_url(val): if not is_url(val): command_line_error(_(f"Target should be url, not directory. Got '{val}' instead.")) return val def check_time(val): return _check_time(val) def check_timeout(val): """ set timeout for backends """ val = _check_int(val) socket.setdefaulttimeout(val) return val def check_verbosity(val): fail = False verb = log.NOTICE val = val.lower() if val in ["e", "error"]: verb = log.ERROR elif val in ["w", "warning"]: verb = log.WARNING elif val in ["n", "notice"]: verb = log.NOTICE elif val in ["i", "info"]: verb = log.INFO elif val in ["d", "debug"]: verb = log.DEBUG else: try: verb = int(val) if verb < 0 or verb > 9: fail = True except ValueError: fail = True if fail: # TRANSL: In this portion of the usage instructions, "[ewnid]" indicates which # characters are permitted (e, w, n, i, or d); the brackets imply their own # meaning in regex; i.e., only one of the characters is allowed in an instance. command_line_error( _( "Verbosity must be one of: digit [0-9], character [ewnid],\n" "or word ['error', 'warning', 'notice', 'info', 'debug'].\n" "The default is 3 (Notice). It is strongly recommended\n" "that verbosity level is set at 2 (Warning) or higher." ) ) log.setverbosity(verb) return verb def dflt(val): """ Return printable value for default. """ return val def expand_fn(filename): return os.path.expanduser(os.path.expandvars(filename)) def expand_archive_dir(archdir, backname): """ Return expanded version of archdir joined with backname. """ assert config.backup_name is not False, "expand_archive_dir() called prior to config.backup_name being set" return expand_fn(os.path.join(archdir, os.fsencode(backname))) def generate_default_backup_name(backend_url): """ @param backend_url: URL to backend. @returns A default backup name (string). """ # For default, we hash args to obtain a reasonably safe default. # We could be smarter and resolve things like relative paths, but # this should actually be a pretty good compromise. Normally only # the destination will matter since you typically only restart # backups of the same thing to a given destination. The inclusion # of the source however, does protect against most changes of # source directory (for whatever reason, such as # /path/to/different/snapshot). If the user happens to have a case # where relative paths are used yet the relative path is the same # (but duplicity is run from a different directory or similar), # then it is simply up to the user to set --archive-dir properly. burlhash = md5() burlhash.update(backend_url.encode()) return burlhash.hexdigest() def is_url(val): """ Check if val is URL """ return len(val.splitlines()) <= 1 and url_regexp.match(val) def is_path(val): """ Check if val is PATH """ return not is_url(val) def make_bytes(value): if isinstance(value, str): return bytes(value, "utf-8") def var2cmd(s): """ Convert var name to command string """ return s.replace("_", "-") def var2opt(s): """ Convert var name to option string """ if len(s) > 1: return f"--{s.replace('_', '-')}" else: return f"-{s}" def cmd2var(s): """ Convert command string to var name """ return s.replace("-", "_") def opt2var(s): """ Convert option string to var name """ return s.lstrip("-").replace("-", "_") def set_log_fd(fd): try: fd = int(fd) except ValueError: command_line_error("log_fd must be an integer.") if fd < 1: command_line_error("log-fd must be greater than zero.") log.add_fd(fd) return fd def set_log_file(fn): fn = check_file(fn) log.add_file(fn) return fn def set_kilos(num): return _check_int(num) * 1024 def set_megs(num): return _check_int(num) * 1024 * 1024 def set_archive_dir(dirstring): """Check archive dir and set global""" if not os.path.exists(dirstring): try: os.makedirs(dirstring) except Exception: pass archive_dir_path = path.Path(dirstring) if not archive_dir_path.isdir(): command_line_error(_(f"Specified archive directory '{archive_dir_path.uc_name}' is not a directory")) config.archive_dir_path = archive_dir_path def set_encrypt_key(encrypt_key): """Set config.gpg_profile.encrypt_key assuming proper key given""" if not gpg_key_patt.match(encrypt_key): command_line_error( _( f"Encrypt key should be an 8, 16, or 40 character hex string, like 'AA0E73D2'.\n" f"Received '{encrypt_key}' length={len(encrypt_key)} instead." ) ) if config.gpg_profile.recipients is None: config.gpg_profile.recipients = [] config.gpg_profile.recipients.append(encrypt_key) def set_encrypt_sign_key(encrypt_sign_key): """Set config.gpg_profile.encrypt_sign_key assuming proper key given""" set_encrypt_key(encrypt_sign_key) set_sign_key(encrypt_sign_key) def set_hidden_encrypt_key(hidden_encrypt_key): """Set config.gpg_profile.hidden_encrypt_key assuming proper key given""" if not gpg_key_patt.match(hidden_encrypt_key): command_line_error( _( f"Hidden dncrypt key should be an 8, 16, or 40 character hex string, like 'AA0E73D2'.\n" f"Received '{hidden_encrypt_key}' length={len(hidden_encrypt_key)} instead." ) ) if config.gpg_profile.hidden_recipients is None: config.gpg_profile.hidden_recipients = [] config.gpg_profile.hidden_recipients.append(hidden_encrypt_key) def set_sign_key(sign_key): """Set config.gpg_profile.sign_key assuming proper key given""" if not gpg_key_patt.match(sign_key): command_line_error( _( f"Sign key should be an 8, 16, or 40 character hex string, like 'AA0E73D2'.\n" f"Received '{sign_key}' length={len(sign_key)} instead." ) ) config.gpg_profile.sign_key = sign_key def set_selection(): """Return selection iter starting at filename with arguments applied""" sel = selection.Select(config.local_path) sel.ParseArgs(config.select_opts, config.select_files) config.select = sel.set_iter()