"Fossies" - the Fresh Open Source Software Archive

Member "backintime-1.2.0/common/backintime.py" (27 Apr 2019, 43026 Bytes) of package /linux/privat/backintime-1.2.0.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "backintime.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 1.1.24_vs_1.2.0.

    1 #    Back In Time
    2 #    Copyright (C) 2008-2019 Oprea Dan, Bart de Koning, Richard Bailey, Germar Reitze
    3 #
    4 #    This program is free software; you can redistribute it and/or modify
    5 #    it under the terms of the GNU General Public License as published by
    6 #    the Free Software Foundation; either version 2 of the License, or
    7 #    (at your option) any later version.
    8 #
    9 #    This program is distributed in the hope that it will be useful,
   10 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
   11 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   12 #    GNU General Public License for more details.
   13 #
   14 #    You should have received a copy of the GNU General Public License along
   15 #    with this program; if not, write to the Free Software Foundation, Inc.,
   16 #    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
   17 
   18 import os
   19 import sys
   20 import gettext
   21 import argparse
   22 import atexit
   23 import subprocess
   24 from datetime import datetime
   25 from time import sleep
   26 
   27 import config
   28 import logger
   29 import snapshots
   30 import tools
   31 import sshtools
   32 import mount
   33 import password
   34 import encfstools
   35 import cli
   36 from exceptions import MountException
   37 from applicationinstance import ApplicationInstance
   38 
   39 _=gettext.gettext
   40 
   41 RETURN_OK = 0
   42 RETURN_ERR = 1
   43 RETURN_NO_CFG = 2
   44 
   45 parsers = {}
   46 
   47 def takeSnapshotAsync(cfg, checksum = False):
   48     """
   49     Fork a new backintime process with 'backup' command which will
   50     take a new snapshot in background.
   51 
   52     Args:
   53         cfg (config.Config): config that should be used
   54     """
   55     cmd = []
   56     if cfg.ioniceOnUser():
   57         cmd.extend(('ionice', '-c2', '-n7'))
   58     cmd.append('backintime')
   59     if '1' != cfg.currentProfile():
   60         cmd.extend(('--profile-id', str(cfg.currentProfile())))
   61     if cfg._LOCAL_CONFIG_PATH is not cfg._DEFAULT_CONFIG_PATH:
   62         cmd.extend(('--config', cfg._LOCAL_CONFIG_PATH))
   63     if cfg._LOCAL_DATA_FOLDER is not cfg._DEFAULT_LOCAL_DATA_FOLDER:
   64         cmd.extend(('--share-path', cfg.DATA_FOLDER_ROOT))
   65     if logger.DEBUG:
   66         cmd.append('--debug')
   67     if checksum:
   68         cmd.append('--checksum')
   69     cmd.append('backup')
   70 
   71     # child process need to start its own ssh-agent because otherwise
   72     # it would be lost without ssh-agent if parent will close
   73     env = os.environ.copy()
   74     for i in ('SSH_AUTH_SOCK', 'SSH_AGENT_PID'):
   75         try:
   76             del env[i]
   77         except:
   78             pass
   79     subprocess.Popen(cmd, env = env)
   80 
   81 def takeSnapshot(cfg, force = True):
   82     """
   83     Take a new snapshot.
   84 
   85     Args:
   86         cfg (config.Config):    config that should be used
   87         force (bool):           take the snapshot even if it wouldn't need to
   88                                 or would be prevented (e.g. running on battery)
   89 
   90     Returns:
   91         bool:                   ``True`` if there was an error
   92     """
   93     tools.envLoad(cfg.cronEnvFile())
   94     ret = snapshots.Snapshots(cfg).backup(force)
   95     return ret
   96 
   97 def _mount(cfg):
   98     """
   99     Mount external filesystems.
  100 
  101     Args:
  102         cfg (config.Config):    config that should be used
  103     """
  104     try:
  105         hash_id = mount.Mount(cfg = cfg).mount()
  106     except MountException as ex:
  107         logger.error(str(ex))
  108         sys.exit(RETURN_ERR)
  109     else:
  110         cfg.setCurrentHashId(hash_id)
  111 
  112 def _umount(cfg):
  113     """
  114     Unmount external filesystems.
  115 
  116     Args:
  117         cfg (config.Config):    config that should be used
  118     """
  119     try:
  120         mount.Mount(cfg = cfg).umount(cfg.current_hash_id)
  121     except MountException as ex:
  122         logger.error(str(ex))
  123 
  124 def createParsers(app_name = 'backintime'):
  125     """
  126     Define parsers for commandline arguments.
  127 
  128     Args:
  129         app_name (str):         string representing the current application
  130     """
  131     global parsers
  132 
  133     #define debug
  134     debugArgsParser = argparse.ArgumentParser(add_help = False)
  135     debugArgsParser.add_argument('--debug',
  136                                  action = 'store_true',
  137                                  help = 'Increase verbosity.')
  138 
  139     #define config argument
  140     configArgsParser = argparse.ArgumentParser(add_help = False)
  141     configArgsParser.add_argument('--config',
  142                                  metavar = 'PATH',
  143                                  type = str,
  144                                  action = 'store',
  145                                  help = 'Read config from %(metavar)s. ' +
  146                                         'Default = ~/.config/backintime/config')
  147 
  148     configArgsParser.add_argument('--share-path',
  149                                  metavar = 'PATH',
  150                                  type = str,
  151                                  action = 'store',
  152                                  help = 'Write runtime data (locks, messages, log and mountpoints) to %(metavar)s.')
  153 
  154     #define common arguments which are used for all commands
  155     commonArgsParser = argparse.ArgumentParser(add_help = False, parents = [configArgsParser, debugArgsParser])
  156     profileGroup = commonArgsParser.add_mutually_exclusive_group()
  157     profileGroup.add_argument    ('--profile',
  158                                   metavar = 'NAME',
  159                                   type = str,
  160                                   action = 'store',
  161                                   help = 'Select profile by %(metavar)s.')
  162     profileGroup.add_argument    ('--profile-id',
  163                                   metavar = 'ID',
  164                                   type = int,
  165                                   action = 'store',
  166                                   help = 'Select profile by %(metavar)s.')
  167     commonArgsParser.add_argument('--quiet',
  168                                   action = 'store_true',
  169                                   help = 'Be quiet. Suppress messages on stdout.')
  170 
  171     #define arguments which are only used by snapshots-path, snapshots-list-path and last-snapshot-path
  172     snapshotPathParser = argparse.ArgumentParser(add_help = False)
  173     snapshotPathParser.add_argument('--keep-mount',
  174                                     action = 'store_true',
  175                                     help = "Don't unmount on exit.")
  176 
  177     #define arguments which are used by rsync commands (backup and restore)
  178     rsyncArgsParser = argparse.ArgumentParser(add_help = False)
  179     rsyncArgsParser.add_argument('--checksum',
  180                                  action = 'store_true',
  181                                  help = 'force to use checksum for checking if files have been changed.')
  182 
  183     #define arguments for snapshot remove
  184     removeArgsParser = argparse.ArgumentParser(add_help = False)
  185     removeArgsParser.add_argument('SNAPSHOT_ID',
  186                                   type = str,
  187                                   action = 'store',
  188                                   nargs = '*',
  189                                   help = 'ID of snapshots which should be removed.')
  190 
  191     #define main argument parser
  192     parser = argparse.ArgumentParser(prog = app_name,
  193                                      parents = [commonArgsParser],
  194                                      description = '%(app)s - a simple backup tool for Linux.'
  195                                                    % {'app': config.Config.APP_NAME},
  196                                      epilog = "For backwards compatibility commands can also be used with trailing '--'. "
  197                                               "All listed arguments will work with all commands. Some commands have extra arguments. "
  198                                               "Run '%(app_name)s <COMMAND> -h' to see the extra arguments."
  199                                               % {'app_name': app_name})
  200     parsers['main'] = parser
  201     parser.add_argument('--version', '-v',
  202                         action = 'version',
  203                         version = '%(prog)s ' + str(config.Config.VERSION),
  204                         help = "show %(prog)s's version number.")
  205     parser.add_argument('--license',
  206                         action = printLicense,
  207                         nargs = 0,
  208                         help = "show %(prog)s's license.")
  209 
  210     #######################
  211     ### define commands ###
  212     #######################
  213     epilog = "Run '%(app_name)s -h' to get help for additional arguments. " %{'app_name': app_name}
  214     epilogCommon = epilog + 'Additional arguments: --config, --debug, --profile, --profile-id, --quiet'
  215     epilogConfig = epilog + 'Additional arguments: --config, --debug'
  216 
  217     subparsers = parser.add_subparsers(title = 'Commands', dest = 'command')
  218     command = 'backup'
  219     nargs = 0
  220     aliases = [(command, nargs), ('b', nargs)]
  221     description = 'Take a new snapshot. Ignore if the profile ' +\
  222                   'is not scheduled or if the machine runs on battery.'
  223     backupCP =             subparsers.add_parser(command,
  224                                                  parents = [rsyncArgsParser],
  225                                                  epilog = epilogCommon,
  226                                                  help = description,
  227                                                  description = description)
  228     backupCP.set_defaults(func = backup)
  229     parsers[command] = backupCP
  230 
  231     command = 'backup-job'
  232     nargs = 0
  233     aliases.append((command, nargs))
  234     description = 'Take a new snapshot in background only '      +\
  235                   'if the profile is scheduled and the machine ' +\
  236                   'is not on battery. This is use by cron jobs.'
  237     backupJobCP =          subparsers.add_parser(command,
  238                                                  parents = [rsyncArgsParser],
  239                                                  epilog = epilogCommon,
  240                                                  help = description,
  241                                                  description = description)
  242     backupJobCP.set_defaults(func = backupJob)
  243     parsers[command] = backupJobCP
  244 
  245     command = 'benchmark-cipher'
  246     nargs = '?'
  247     aliases.append((command, nargs))
  248     description = 'Show a benchmark of all ciphers for ssh transfer.'
  249     benchmarkCipherCP =    subparsers.add_parser(command,
  250                                                  epilog = epilogCommon,
  251                                                  help = description,
  252                                                  description = description)
  253     benchmarkCipherCP.set_defaults(func = benchmarkCipher)
  254     parsers[command] = benchmarkCipherCP
  255     benchmarkCipherCP.add_argument              ('FILE_SIZE',
  256                                                  type = int,
  257                                                  action = 'store',
  258                                                  default = 40,
  259                                                  nargs = '?',
  260                                                  help = 'File size used to for benchmark.')
  261 
  262     command = 'check-config'
  263     description = 'Check the profiles configuration and install crontab entries.'
  264     checkConfigCP =        subparsers.add_parser(command,
  265                                                  epilog = epilogCommon,
  266                                                  help = description,
  267                                                  description = description)
  268     checkConfigCP.add_argument                  ('--no-crontab',
  269                                                  action = 'store_true',
  270                                                  help = 'Do not install crontab entries.')
  271     checkConfigCP.set_defaults(func = checkConfig)
  272     parsers[command] = checkConfigCP
  273 
  274     command = 'decode'
  275     nargs = '*'
  276     aliases.append((command, nargs))
  277     description = "Decode paths with 'encfsctl decode'"
  278     decodeCP =             subparsers.add_parser(command,
  279                                                  epilog = epilogCommon,
  280                                                  help = description,
  281                                                  description = description)
  282     decodeCP.set_defaults(func = decode)
  283     parsers[command] = decodeCP
  284     decodeCP.add_argument                       ('PATH',
  285                                                  type = str,
  286                                                  action = 'store',
  287                                                  nargs = '*',
  288                                                  help = 'Decode PATH. If no PATH is specified on command line ' +\
  289                                                  'a list of filenames will be read from stdin.')
  290 
  291     command = 'last-snapshot'
  292     nargs = 0
  293     aliases.append((command, nargs))
  294     description = 'Show the ID of the last snapshot.'
  295     lastSnapshotCP =       subparsers.add_parser(command,
  296                                                  epilog = epilogCommon,
  297                                                  help = description,
  298                                                  description = description)
  299     lastSnapshotCP.set_defaults(func = lastSnapshot)
  300     parsers[command] = lastSnapshotCP
  301 
  302     command = 'last-snapshot-path'
  303     nargs = 0
  304     aliases.append((command, nargs))
  305     description = 'Show the path of the last snapshot.'
  306     lastSnapshotsPathCP =  subparsers.add_parser(command,
  307                                                  parents = [snapshotPathParser],
  308                                                  epilog = epilogCommon,
  309                                                  help = description,
  310                                                  description = description)
  311     lastSnapshotsPathCP.set_defaults(func = lastSnapshotPath)
  312     parsers[command] = lastSnapshotsPathCP
  313 
  314     command = 'pw-cache'
  315     nargs = '*'
  316     aliases.append((command, nargs))
  317     description = 'Control Password Cache for non-interactive cronjobs.'
  318     pwCacheCP =            subparsers.add_parser(command,
  319                                                  epilog = epilogConfig,
  320                                                  help = description,
  321                                                  description = description)
  322     pwCacheCP.set_defaults(func = pwCache)
  323     parsers[command] = pwCacheCP
  324     pwCacheCP.add_argument                      ('ACTION',
  325                                                  action = 'store',
  326                                                  choices = ['start', 'stop', 'restart', 'reload', 'status'],
  327                                                  nargs = '?',
  328                                                  help = 'Command to send to Password Cache daemon.')
  329 
  330     command = 'remove'
  331     nargs = '*'
  332     aliases.append((command, nargs))
  333     description = 'Remove a snapshot.'
  334     removeCP =             subparsers.add_parser(command,
  335                                                  parents = [removeArgsParser],
  336                                                  epilog = epilogCommon,
  337                                                  help = description,
  338                                                  description = description)
  339     removeCP.set_defaults(func = remove)
  340     parsers[command] = removeCP
  341 
  342     command = 'remove-and-do-not-ask-again'
  343     nargs = '*'
  344     aliases.append((command, nargs))
  345     description = "Remove snapshots and don't ask for confirmation before. Be careful!"
  346     removeDoNotAskCP =     subparsers.add_parser(command,
  347                                                  parents = [removeArgsParser],
  348                                                  epilog = epilogCommon,
  349                                                  help = description,
  350                                                  description = description)
  351     removeDoNotAskCP.set_defaults(func = removeAndDoNotAskAgain)
  352     parsers[command] = removeDoNotAskCP
  353 
  354     command = 'restore'
  355     nargs = '*'
  356     aliases.append((command, nargs))
  357     description = 'Restore files.'
  358     restoreCP =            subparsers.add_parser(command,
  359                                                  parents = [rsyncArgsParser],
  360                                                  epilog = epilogCommon,
  361                                                  help = description,
  362                                                  description = description)
  363     restoreCP.set_defaults(func = restore)
  364     parsers[command] = restoreCP
  365     backupGroup = restoreCP.add_mutually_exclusive_group()
  366     restoreCP.add_argument                      ('WHAT',
  367                                                  type = str,
  368                                                  action = 'store',
  369                                                  nargs = '?',
  370                                                  help = 'Restore file or folder WHAT.')
  371 
  372     restoreCP.add_argument                      ('WHERE',
  373                                                  type = str,
  374                                                  action = 'store',
  375                                                  nargs = '?',
  376                                                  help = "Restore to WHERE. An empty argument '' will restore to original destination.")
  377 
  378     restoreCP.add_argument                      ('SNAPSHOT_ID',
  379                                                  type = str,
  380                                                  action = 'store',
  381                                                  nargs = '?',
  382                                                  help = 'Which SNAPSHOT_ID should be used. This can be a snapshot ID or ' +\
  383                                                  'an integer starting with 0 for the last snapshot, 1 for the second to last, ... ' +\
  384                                                  'the very first snapshot is -1')
  385 
  386     restoreCP.add_argument                      ('--delete',
  387                                                  action = 'store_true',
  388                                                  help = 'Restore and delete newer files which are not in the snapshot. ' +\
  389                                                  'WARNING: deleting files in filesystem root could break your whole system!!!')
  390 
  391     backupGroup.add_argument                    ('--local-backup',
  392                                                  action = 'store_true',
  393                                                  help = 'Create backup files before changing local files.')
  394 
  395     backupGroup.add_argument                    ('--no-local-backup',
  396                                                  action = 'store_true',
  397                                                  help = 'Temporary disable creation of backup files before changing local files. ' +\
  398                                                  'This can be switched of permanently in Settings, too.')
  399 
  400     restoreCP.add_argument                      ('--only-new',
  401                                                  action = 'store_true',
  402                                                  help = 'Only restore files which does not exist or are newer than ' +\
  403                                                         'those in destination. Using "rsync --update" option.')
  404 
  405     command = 'shutdown'
  406     nargs = 0
  407     description = 'Shutdown the computer after the snapshot is done.'
  408     shutdownCP =           subparsers.add_parser(command,
  409                                                  epilog = epilogCommon,
  410                                                  help = description,
  411                                                  description = description)
  412     shutdownCP.set_defaults(func = shutdown)
  413     parsers[command] = shutdownCP
  414 
  415     command = 'smart-remove'
  416     nargs = 0
  417     description = 'Remove snapshots based on "Smart Remove" pattern.'
  418     smartRemoveCP =        subparsers.add_parser(command,
  419                                                  epilog = epilogCommon,
  420                                                  help = description,
  421                                                  description = description)
  422     smartRemoveCP.set_defaults(func = smartRemove)
  423     parsers[command] = smartRemoveCP
  424 
  425     command = 'snapshots-list'
  426     nargs = 0
  427     aliases.append((command, nargs))
  428     description = 'Show a list of snapshots IDs.'
  429     snapshotsListCP =      subparsers.add_parser(command,
  430                                                  parents = [snapshotPathParser],
  431                                                  epilog = epilogCommon,
  432                                                  help = description,
  433                                                  description = description)
  434     snapshotsListCP.set_defaults(func = snapshotsList)
  435     parsers[command] = snapshotsListCP
  436 
  437     command = 'snapshots-list-path'
  438     nargs = 0
  439     aliases.append((command, nargs))
  440     description = "Show the path's to snapshots."
  441     snapshotsListPathCP =  subparsers.add_parser(command,
  442                                                  parents = [snapshotPathParser],
  443                                                  epilog = epilogCommon,
  444                                                  help = description,
  445                                                  description = description)
  446     snapshotsListPathCP.set_defaults(func = snapshotsListPath)
  447     parsers[command] = snapshotsListPathCP
  448 
  449     command = 'snapshots-path'
  450     nargs = 0
  451     aliases.append((command, nargs))
  452     description = 'Show the path where snapshots are stored.'
  453     snapshotsPathCP =      subparsers.add_parser(command,
  454                                                  parents = [snapshotPathParser],
  455                                                  epilog = epilogCommon,
  456                                                  help = description,
  457                                                  description = description)
  458     snapshotsPathCP.set_defaults(func = snapshotsPath)
  459     parsers[command] = snapshotsPathCP
  460 
  461     command = 'unmount'
  462     nargs = 0
  463     aliases.append((command, nargs))
  464     description = 'Unmount the profile.'
  465     unmountCP =            subparsers.add_parser(command,
  466                                                  epilog = epilogCommon,
  467                                                  help = description,
  468                                                  description = description)
  469     unmountCP.set_defaults(func = unmount)
  470     parsers[command] = unmountCP
  471 
  472     #define aliases for all commands with trailing --
  473     group = parser.add_mutually_exclusive_group()
  474     for alias, nargs in aliases:
  475         if len(alias) == 1:
  476             arg = '-%s' % alias
  477         else:
  478             arg = '--%s' % alias
  479         group.add_argument(arg,
  480                            nargs = nargs,
  481                            action = PseudoAliasAction,
  482                            help = argparse.SUPPRESS)
  483 
  484 def startApp(app_name = 'backintime'):
  485     """
  486     Start the requested command or return config if there was no command
  487     in arguments.
  488 
  489     Args:
  490         app_name (str): string representing the current application
  491 
  492     Returns:
  493         config.Config:  current config if no command was given in arguments
  494     """
  495     createParsers(app_name)
  496     #open log
  497     logger.APP_NAME = app_name
  498     logger.openlog()
  499 
  500     #parse args
  501     args = argParse(None)
  502 
  503     #add source path to $PATH environ if running from source
  504     if tools.runningFromSource():
  505         tools.addSourceToPathEnviron()
  506 
  507     #warn about sudo
  508     if tools.usingSudo() and os.getenv('BIT_SUDO_WARNING_PRINTED', 'false') == 'false':
  509         os.putenv('BIT_SUDO_WARNING_PRINTED', 'true')
  510         logger.warning("It looks like you're using 'sudo' to start %(app)s. "
  511                        "This will cause some troubles. Please use either 'sudo -i %(app_name)s' "
  512                        "or 'pkexec %(app_name)s'."
  513                        %{'app_name': app_name, 'app': config.Config.APP_NAME})
  514 
  515     #call commands
  516     if 'func' in dir(args):
  517         args.func(args)
  518     else:
  519         setQuiet(args)
  520         printHeader()
  521         return getConfig(args, False)
  522 
  523 def argParse(args):
  524     """
  525     Parse arguments given on commandline.
  526 
  527     Args:
  528         args (argparse.Namespace):  Namespace that should be enhanced
  529                                     or ``None``
  530 
  531     Returns:
  532         argparser.Namespace:        new parsed Namespace
  533     """
  534     def join(args, subArgs):
  535         """
  536         Add new arguments to existing Namespace.
  537 
  538         Args:
  539             args (argparse.Namespace):
  540                         main Namespace that should get new arguments
  541             subArgs (argparse.Namespace):
  542                         second Namespace which have new arguments
  543                         that should be merged into ``args``
  544         """
  545         for key, value in vars(subArgs).items():
  546             #only add new values if it isn't set already or if there really IS a value
  547             if getattr(args, key, None) is None or value:
  548                 setattr(args, key, value)
  549 
  550     #first parse the main parser without subparsers
  551     #otherwise positional args in subparsers will be to greedy
  552     #but only if -h or --help is not involved because otherwise
  553     #help will not work for subcommands
  554     mainParser = parsers['main']
  555     sub = []
  556     if '-h' not in sys.argv and '--help' not in sys.argv:
  557         for i in mainParser._actions:
  558             if isinstance(i, argparse._SubParsersAction):
  559                 #remove subparsers
  560                 mainParser._remove_action(i)
  561                 sub.append(i)
  562     args, unknownArgs = mainParser.parse_known_args(args)
  563     #read subparsers again
  564     if sub:
  565         [mainParser._add_action(i) for i in sub]
  566 
  567     #parse it again for unknown args
  568     if unknownArgs:
  569         subArgs, unknownArgs = mainParser.parse_known_args(unknownArgs)
  570         join(args, subArgs)
  571 
  572     #finally parse only the command parser, otherwise we miss
  573     #some arguments from command
  574     if unknownArgs and 'command' in args and args.command in parsers:
  575         commandParser = parsers[args.command]
  576         subArgs, unknownArgs = commandParser.parse_known_args(unknownArgs)
  577         join(args, subArgs)
  578 
  579     if 'debug' in args:
  580         logger.DEBUG = args.debug
  581 
  582     dargs = vars(args)
  583     logger.debug('Arguments: %s | unknownArgs: %s'
  584                  %({arg:dargs[arg] for arg in dargs if dargs[arg]},
  585                    unknownArgs))
  586 
  587     #report unknown arguments
  588     #but not if we run aliasParser next because we will parse again in there
  589     if unknownArgs and not ('func' in args and args.func is aliasParser):
  590         mainParser.error('Unknown Argument(s): %s' % ', '.join(unknownArgs))
  591     return args
  592 
  593 def printHeader():
  594     """
  595     Print application name, version and legal notes.
  596     """
  597     version = config.Config.VERSION
  598     ref, hashid = tools.gitRevisionAndHash()
  599     if ref:
  600         version += " git branch '{}' hash '{}'".format(ref, hashid)
  601     print('')
  602     print('Back In Time')
  603     print('Version: ' + version)
  604     print('')
  605     print('Back In Time comes with ABSOLUTELY NO WARRANTY.')
  606     print('This is free software, and you are welcome to redistribute it')
  607     print("under certain conditions; type `backintime --license' for details.")
  608     print('')
  609 
  610 class PseudoAliasAction(argparse.Action):
  611     """
  612     Translate '--COMMAND' into 'COMMAND' for backwards compatibility.
  613     """
  614     def __call__(self, parser, namespace, values, option_string=None):
  615         """
  616         Translate '--COMMAND' into 'COMMAND' for backwards compatibility.
  617 
  618         Args:
  619             parser (argparse.ArgumentParser): NotImplemented
  620             namespace (argparse.Namespace):   Namespace that should get modified
  621             values:                           NotImplemented
  622             option_string:                    NotImplemented
  623         """
  624         #TODO: find a more elegant way to solve this
  625         dest = self.dest.replace('_', '-')
  626         if self.dest == 'b':
  627             replace = '-b'
  628             alias = 'backup'
  629         else:
  630             replace = '--%s' % dest
  631             alias = dest
  632         setattr(namespace, 'func', aliasParser)
  633         setattr(namespace, 'replace', replace)
  634         setattr(namespace, 'alias', alias)
  635 
  636 def aliasParser(args):
  637     """
  638     Call commands which where given with leading -- for backwards
  639     compatibility.
  640 
  641     Args:
  642         args (argparse.Namespace):
  643                         previously parsed arguments
  644     """
  645     if not args.quiet:
  646         logger.info("Run command '%(alias)s' instead of argument '%(replace)s' due to backwards compatibility."
  647                     % {'alias': args.alias, 'replace': args.replace})
  648     argv = [w.replace(args.replace, args.alias) for w in sys.argv[1:]]
  649     newArgs = argParse(argv)
  650     if 'func' in dir(newArgs):
  651         newArgs.func(newArgs)
  652 
  653 def getConfig(args, check = True):
  654     """
  655     Load config and change to profile selected on commandline.
  656 
  657     Args:
  658         args (argparse.Namespace):
  659                         previously parsed arguments
  660         check (bool):   if ``True`` check if config is valid
  661 
  662     Returns:
  663         config.Config:  current config with requested profile selected
  664 
  665     Raises:
  666         SystemExit:     1 if ``profile`` or ``profile_id`` is no valid profile
  667                         2 if ``check`` is ``True`` and config is not configured
  668     """
  669     cfg = config.Config(config_path = args.config, data_path = args.share_path)
  670     logger.debug('config file: %s' % cfg._LOCAL_CONFIG_PATH)
  671     logger.debug('share path: %s' % cfg._LOCAL_DATA_FOLDER)
  672     logger.debug('profiles: %s' % ', '.join('%s=%s' % (x, cfg.profileName(x))
  673                                                         for x in cfg.profiles()))
  674 
  675     if 'profile_id' in args and args.profile_id:
  676         if not cfg.setCurrentProfile(args.profile_id):
  677             logger.error('Profile-ID not found: %s' % args.profile_id)
  678             sys.exit(RETURN_ERR)
  679     if 'profile' in args and args.profile:
  680         if not cfg.setCurrentProfileByName(args.profile):
  681             logger.error('Profile not found: %s' % args.profile)
  682             sys.exit(RETURN_ERR)
  683     if check and not cfg.isConfigured():
  684         logger.error('%(app)s is not configured!' %{'app': cfg.APP_NAME})
  685         sys.exit(RETURN_NO_CFG)
  686     if 'checksum' in args:
  687         cfg.forceUseChecksum = args.checksum
  688     return cfg
  689 
  690 def setQuiet(args):
  691     """
  692     Redirect :py:data:`sys.stdout` to ``/dev/null`` if ``--quiet`` was set on
  693     commandline. Return the original :py:data:`sys.stdout` file object which can
  694     be used to print absolute necessary information.
  695 
  696     Args:
  697         args (argparse.Namespace):
  698                         previously parsed arguments
  699 
  700     Returns:
  701         sys.stdout:     default sys.stdout
  702     """
  703     force_stdout = sys.stdout
  704     if args.quiet:
  705         # do not replace with subprocess.DEVNULL - will not work
  706         sys.stdout = open(os.devnull, 'w')
  707         atexit.register(sys.stdout.close)
  708         atexit.register(force_stdout.close)
  709     return force_stdout
  710 
  711 class printLicense(argparse.Action):
  712     """
  713     Print custom license
  714     """
  715     def __init__(self, *args, **kwargs):
  716         super(printLicense, self).__init__(*args, **kwargs)
  717 
  718     def __call__(self, *args, **kwargs):
  719         cfg = config.Config()
  720         print(cfg.license())
  721         sys.exit(RETURN_OK)
  722 
  723 def backup(args, force = True):
  724     """
  725     Command for force taking a new snapshot.
  726 
  727     Args:
  728         args (argparse.Namespace):
  729                         previously parsed arguments
  730         force (bool):   take the snapshot even if it wouldn't need to or would
  731                         be prevented (e.g. running on battery)
  732 
  733     Raises:
  734         SystemExit:     0 if successful, 1 if not
  735     """
  736     setQuiet(args)
  737     printHeader()
  738     cfg = getConfig(args)
  739     ret = takeSnapshot(cfg, force)
  740     sys.exit(int(ret))
  741 
  742 def backupJob(args):
  743     """
  744     Command for taking a new snapshot in background. Mainly used for cronjobs.
  745     This will run the snapshot inside a daemon and detach from it. It will
  746     return immediately back to commandline.
  747 
  748     Args:
  749         args (argparse.Namespace):
  750                         previously parsed arguments
  751 
  752     Raises:
  753         SystemExit:     0
  754     """
  755     cli.BackupJobDaemon(backup, args).start()
  756 
  757 def shutdown(args):
  758     """
  759     Command for shutting down the computer after the current snapshot has
  760     finished.
  761 
  762     Args:
  763         args (argparse.Namespace):
  764                         previously parsed arguments
  765 
  766     Raises:
  767         SystemExit:     0 if successful; 1 if it failed either because there is
  768                         no active snapshot for this profile or shutdown is not
  769                         supported.
  770     """
  771     setQuiet(args)
  772     printHeader()
  773     cfg = getConfig(args)
  774 
  775     sd = tools.ShutDown()
  776     if not sd.canShutdown():
  777         logger.warning('Shutdown is not supported.')
  778         sys.exit(RETURN_ERR)
  779 
  780     instance = ApplicationInstance(cfg.takeSnapshotInstanceFile(), False)
  781     profile = '='.join((cfg.currentProfile(), cfg.profileName()))
  782     if not instance.busy():
  783         logger.info('There is no active snapshot for profile %s. Skip shutdown.'
  784                     %profile)
  785         sys.exit(RETURN_ERR)
  786 
  787     print('Shutdown is waiting for the snapshot in profile %s to end.\nPress CTRL+C to interrupt shutdown.\n'
  788           %profile)
  789     sd.activate_shutdown = True
  790     try:
  791         while instance.busy():
  792             logger.debug('Snapshot is still active. Wait for shutdown.')
  793             sleep(5)
  794     except KeyboardInterrupt:
  795         print('Shutdown interrupted.')
  796     else:
  797         logger.info('Shutdown now.')
  798         sd.shutdown()
  799     sys.exit(RETURN_OK)
  800 
  801 def snapshotsPath(args):
  802     """
  803     Command for printing the full snapshot path of current profile.
  804 
  805     Args:
  806         args (argparse.Namespace):
  807                         previously parsed arguments
  808 
  809     Raises:
  810         SystemExit:     0
  811     """
  812     force_stdout = setQuiet(args)
  813     cfg = getConfig(args)
  814     if args.keep_mount:
  815         _mount(cfg)
  816     if args.quiet:
  817         msg = '{}'
  818     else:
  819         msg = 'SnapshotsPath: {}'
  820     print(msg.format(cfg.snapshotsFullPath()), file=force_stdout)
  821     sys.exit(RETURN_OK)
  822 
  823 def snapshotsList(args):
  824     """
  825     Command for printing a list of all snapshots in current profile.
  826 
  827     Args:
  828         args (argparse.Namespace):
  829                         previously parsed arguments
  830 
  831     Raises:
  832         SystemExit:     0
  833     """
  834     force_stdout = setQuiet(args)
  835     cfg = getConfig(args)
  836     _mount(cfg)
  837 
  838     if args.quiet:
  839         msg = '{}'
  840     else:
  841         msg = 'SnapshotID: {}'
  842     no_sids = True
  843     #use snapshots.listSnapshots instead of iterSnapshots because of sorting
  844     for sid in snapshots.listSnapshots(cfg, reverse = False):
  845         print(msg.format(sid), file=force_stdout)
  846         no_sids = False
  847     if no_sids:
  848         logger.error("There are no snapshots in '%s'" % cfg.profileName())
  849     if not args.keep_mount:
  850         _umount(cfg)
  851     sys.exit(RETURN_OK)
  852 
  853 def snapshotsListPath(args):
  854     """
  855     Command for printing a list of all snapshots pathes in current profile.
  856 
  857     Args:
  858         args (argparse.Namespace):
  859                         previously parsed arguments
  860 
  861     Raises:
  862         SystemExit:     0
  863     """
  864     force_stdout = setQuiet(args)
  865     cfg = getConfig(args)
  866     _mount(cfg)
  867 
  868     if args.quiet:
  869         msg = '{}'
  870     else:
  871         msg = 'SnapshotPath: {}'
  872     no_sids = True
  873     #use snapshots.listSnapshots instead of iterSnapshots because of sorting
  874     for sid in snapshots.listSnapshots(cfg, reverse = False):
  875         print(msg.format(sid.path()), file=force_stdout)
  876         no_sids = False
  877     if no_sids:
  878         logger.error("There are no snapshots in '%s'" % cfg.profileName())
  879     if not args.keep_mount:
  880         _umount(cfg)
  881     sys.exit(RETURN_OK)
  882 
  883 def lastSnapshot(args):
  884     """
  885     Command for printing the very last snapshot in current profile.
  886 
  887     Args:
  888         args (argparse.Namespace):
  889                         previously parsed arguments
  890 
  891     Raises:
  892         SystemExit:     0
  893     """
  894     force_stdout = setQuiet(args)
  895     cfg = getConfig(args)
  896     _mount(cfg)
  897     sid = snapshots.lastSnapshot(cfg)
  898     if sid:
  899         if args.quiet:
  900             msg = '{}'
  901         else:
  902             msg = 'SnapshotID: {}'
  903         print(msg.format(sid), file=force_stdout)
  904     else:
  905         logger.error("There are no snapshots in '%s'" % cfg.profileName())
  906     _umount(cfg)
  907     sys.exit(RETURN_OK)
  908 
  909 def lastSnapshotPath(args):
  910     """
  911     Command for printing the path of the very last snapshot in
  912     current profile.
  913 
  914     Args:
  915         args (argparse.Namespace):
  916                         previously parsed arguments
  917 
  918     Raises:
  919         SystemExit:     0
  920     """
  921     force_stdout = setQuiet(args)
  922     cfg = getConfig(args)
  923     _mount(cfg)
  924     sid = snapshots.lastSnapshot(cfg)
  925     if sid:
  926         if args.quiet:
  927             msg = '{}'
  928         else:
  929             msg = 'SnapshotPath: {}'
  930         print(msg.format(sid.path()), file=force_stdout)
  931     else:
  932         logger.error("There are no snapshots in '%s'" % cfg.profileName())
  933     if not args.keep_mount:
  934         _umount(cfg)
  935     sys.exit(RETURN_OK)
  936 
  937 def unmount(args):
  938     """
  939     Command for unmounting all filesystems.
  940 
  941     Args:
  942         args (argparse.Namespace):
  943                         previously parsed arguments
  944 
  945     Raises:
  946         SystemExit:     0
  947     """
  948     setQuiet(args)
  949     cfg = getConfig(args)
  950     _mount(cfg)
  951     _umount(cfg)
  952     sys.exit(RETURN_OK)
  953 
  954 def benchmarkCipher(args):
  955     """
  956     Command for transferring a file with scp to remote host with all
  957     available ciphers and print its speed and time.
  958 
  959     Args:
  960         args (argparse.Namespace):
  961                         previously parsed arguments
  962 
  963     Raises:
  964         SystemExit:     0
  965     """
  966     setQuiet(args)
  967     printHeader()
  968     cfg = getConfig(args)
  969     if cfg.snapshotsMode() in ('ssh', 'ssh_encfs'):
  970         ssh = sshtools.SSH(cfg)
  971         ssh.benchmarkCipher(args.FILE_SIZE)
  972         sys.exit(RETURN_OK)
  973     else:
  974         logger.error("SSH is not configured for profile '%s'!" % cfg.profileName())
  975         sys.exit(RETURN_ERR)
  976 
  977 def pwCache(args):
  978     """
  979     Command for starting password cache daemon.
  980 
  981     Args:
  982         args (argparse.Namespace):
  983                         previously parsed arguments
  984 
  985     Raises:
  986         SystemExit:     0 if daemon is running, 1 if not
  987     """
  988     force_stdout = setQuiet(args)
  989     printHeader()
  990     cfg = getConfig(args)
  991     ret = RETURN_OK
  992     daemon = password.Password_Cache(cfg)
  993     if args.ACTION and args.ACTION != 'status':
  994         getattr(daemon, args.ACTION)()
  995     elif args.ACTION == 'status':
  996         print('%(app)s Password Cache: ' % {'app': cfg.APP_NAME}, end=' ', file = force_stdout)
  997         if daemon.status():
  998             print(cli.bcolors.OKGREEN + 'running' + cli.bcolors.ENDC, file = force_stdout)
  999             ret = RETURN_OK
 1000         else:
 1001             print(cli.bcolors.FAIL + 'not running' + cli.bcolors.ENDC, file = force_stdout)
 1002             ret = RETURN_ERR
 1003     else:
 1004         daemon.run()
 1005     sys.exit(ret)
 1006 
 1007 def decode(args):
 1008     """
 1009     Command for decoding paths given paths with 'encfsctl'.
 1010     Will listen on stdin if no path was given.
 1011 
 1012     Args:
 1013         args (argparse.Namespace):
 1014                         previously parsed arguments
 1015 
 1016     Raises:
 1017         SystemExit:     0
 1018     """
 1019     force_stdout = setQuiet(args)
 1020     cfg = getConfig(args)
 1021     if cfg.snapshotsMode() not in ('local_encfs', 'ssh_encfs'):
 1022         logger.error("Profile '%s' is not encrypted." % cfg.profileName())
 1023         sys.exit(RETURN_ERR)
 1024     _mount(cfg)
 1025     d = encfstools.Decode(cfg)
 1026     if not args.PATH:
 1027         while True:
 1028             try:
 1029                 path = input()
 1030             except EOFError:
 1031                 break
 1032             if not path:
 1033                 break
 1034             print(d.path(path), file = force_stdout)
 1035     else:
 1036         print('\n'.join(d.list(args.PATH)), file = force_stdout)
 1037     d.close()
 1038     _umount(cfg)
 1039     sys.exit(RETURN_OK)
 1040 
 1041 def remove(args, force = False):
 1042     """
 1043     Command for removing snapshots.
 1044 
 1045     Args:
 1046         args (argparse.Namespace):
 1047                         previously parsed arguments
 1048         force (bool):   don't ask before removing (BE CAREFUL!)
 1049 
 1050     Raises:
 1051         SystemExit:     0
 1052     """
 1053     setQuiet(args)
 1054     printHeader()
 1055     cfg = getConfig(args)
 1056     _mount(cfg)
 1057     cli.remove(cfg, args.SNAPSHOT_ID, force)
 1058     _umount(cfg)
 1059     sys.exit(RETURN_OK)
 1060 
 1061 def removeAndDoNotAskAgain(args):
 1062     """
 1063     Command for removing snapshots without asking before remove
 1064     (BE CAREFUL!)
 1065 
 1066     Args:
 1067         args (argparse.Namespace):
 1068                         previously parsed arguments
 1069 
 1070     Raises:
 1071         SystemExit:     0
 1072     """
 1073     remove(args, True)
 1074 
 1075 def smartRemove(args):
 1076     """
 1077     Command for running Smart-Remove from Terminal.
 1078 
 1079     Args:
 1080         args (argparse.Namespace):
 1081                         previously parsed arguments
 1082 
 1083     Raises:
 1084         SystemExit:     0 if okay
 1085                         2 if Smart-Remove is not configured
 1086     """
 1087     setQuiet(args)
 1088     printHeader()
 1089     cfg = getConfig(args)
 1090     sn = snapshots.Snapshots(cfg)
 1091 
 1092     enabled, keep_all, keep_one_per_day, keep_one_per_week, keep_one_per_month = cfg.smartRemove()
 1093     if enabled:
 1094         _mount(cfg)
 1095         del_snapshots = sn.smartRemoveList(datetime.today(),
 1096                                            keep_all,
 1097                                            keep_one_per_day,
 1098                                            keep_one_per_week,
 1099                                            keep_one_per_month)
 1100         logger.info('Smart Remove will remove {} snapshots'.format(len(del_snapshots)))
 1101         sn.smartRemove(del_snapshots, log = logger.info)
 1102         _umount(cfg)
 1103         sys.exit(RETURN_OK)
 1104     else:
 1105         logger.error('Smart Remove is not configured.')
 1106         sys.exit(RETURN_NO_CFG)
 1107 
 1108 def restore(args):
 1109     """
 1110     Command for restoring files from snapshots.
 1111 
 1112     Args:
 1113         args (argparse.Namespace):
 1114                         previously parsed arguments
 1115 
 1116     Raises:
 1117         SystemExit:     0
 1118     """
 1119     setQuiet(args)
 1120     printHeader()
 1121     cfg = getConfig(args)
 1122     _mount(cfg)
 1123     if cfg.backupOnRestore() and not args.no_local_backup:
 1124         backup = True
 1125     else:
 1126         backup = args.local_backup
 1127     cli.restore(cfg,
 1128                 args.SNAPSHOT_ID,
 1129                 args.WHAT,
 1130                 args.WHERE,
 1131                 delete = args.delete,
 1132                 backup = backup,
 1133                 only_new = args.only_new)
 1134     _umount(cfg)
 1135     sys.exit(RETURN_OK)
 1136 
 1137 def checkConfig(args):
 1138     """
 1139     Command for checking the config file.
 1140 
 1141     Args:
 1142         args (argparse.Namespace):
 1143                         previously parsed arguments
 1144 
 1145     Raises:
 1146         SystemExit:     0 if config is okay, 1 if not
 1147     """
 1148     force_stdout = setQuiet(args)
 1149     printHeader()
 1150     cfg = getConfig(args)
 1151     if cli.checkConfig(cfg, crontab = not args.no_crontab):
 1152         print("\nConfig %(cfg)s profile '%(profile)s' is fine."
 1153               % {'cfg': cfg._LOCAL_CONFIG_PATH,
 1154                  'profile': cfg.profileName()},
 1155               file = force_stdout)
 1156         sys.exit(RETURN_OK)
 1157     else:
 1158         print("\nConfig %(cfg)s profile '%(profile)s' has errors."
 1159               % {'cfg': cfg._LOCAL_CONFIG_PATH,
 1160                  'profile': cfg.profileName()},
 1161               file = force_stdout)
 1162         sys.exit(RETURN_ERR)
 1163 
 1164 if __name__ == '__main__':
 1165     startApp()