# starter.py
#
# Project: AutoArchive
# License: GNU GPLv3
#
# Copyright (C) 2003 - 2022 Róbert Čerňanský
"""Initializes the application and starts it."""
__all__ = ["Starter"]
# {{{ INCLUDES
from abc import ABCMeta, abstractmethod
import os
import sys
from argparse import ArgumentParser, RawDescriptionHelpFormatter, SUPPRESS
from ._meta import _Meta
from AutoArchive._infrastructure.utils import Constants, Utils
from AutoArchive._infrastructure._app_environment import AppEnvironment
from AutoArchive._infrastructure.service._service_accessor import ServiceAccessor
from AutoArchive._infrastructure._application_context import ApplicationContext
from AutoArchive._infrastructure.configuration._configuration_factory import ConfigurationFactory
from AutoArchive._infrastructure.configuration import Options, OptionsUtils, ArchiverTypes
from AutoArchive._infrastructure.storage._file_storage import FileStorage
from AutoArchive._services.archiver._archiver_service_component import ArchiverServiceComponent
from AutoArchive._services.external_command_executor._external_command_executor_service_component import \
ExternalCommandExecutorServiceComponent
from AutoArchive._application.archiving import ArchivingApplication
from AutoArchive._ui.cmdline._user_action_executor import UserActionExecutor
from AutoArchive._ui.cmdline._cmdline_ui import CmdlineUi
from AutoArchive._ui.cmdline._cmdline_commands import CmdlineCommands
# }}} INCLUDES
# {{{ CONSTANTS
#: Tuple of service components
_SERVICE_COMPONENTS = (ExternalCommandExecutorServiceComponent, ArchiverServiceComponent)
# }}} CONSTANTS
# {{{ CLASSES
[docs]class Starter(metaclass = ABCMeta):
"Fires up the show."
@abstractmethod
def __init__(self):
pass
[docs] @classmethod
def start(cls, programArgs = sys.argv):
"Initializes and starts the program."
try:
options, arguments = cls.__parseArguments(programArgs[1:])
appEnvironment = AppEnvironment(os.path.basename(programArgs[0]), options, arguments)
configuration = ConfigurationFactory.makeConfiguration(appEnvironment)
storage = FileStorage(configuration)
applicationContext = ApplicationContext(appEnvironment, configuration, storage)
serviceAccessor = ServiceAccessor()
serviceComponents = cls.__createServiceComponents(applicationContext, serviceAccessor)
cmdlineUi = CmdlineUi(appEnvironment, configuration)
archivingApplication = ArchivingApplication(cmdlineUi, applicationContext, serviceAccessor)
return 0 if UserActionExecutor(cmdlineUi, applicationContext, archivingApplication).execute() else 1
except KeyboardInterrupt:
print("\nAborted by user.")
return 1
except Exception as ex:
import traceback
if Constants.DEBUG:
print(traceback.print_exc())
else:
Utils.printError(f"Exception occurred: {traceback.format_exception_only(type(ex), ex)}.")
return 1
@staticmethod
def __createServiceComponents(applicationContext, serviceAccessor):
serviceComponents = []
for serviceComponentClass in _SERVICE_COMPONENTS:
serviceComponents.append(serviceComponentClass(applicationContext, serviceAccessor))
return serviceComponents
@classmethod
def __parseArguments(cls, programArgs):
"Parses command line arguments."
# define usage and version strings
usage = "%(prog)s [options] [command] [AA_SPEC ...]"
description = _Meta.DESCRIPTION
version = """\
%(prog)s version {version}
{copyright}
{license}
""".format(version = _Meta.VERSION, copyright = _Meta.COPYRIGHT, license = _Meta.LICENSE)
# create parser and add the options
# we use RawDescriptionHelpFormatter to preserve format of 'version' text
parser = ArgumentParser(usage = usage, description = description, add_help = False,
formatter_class = RawDescriptionHelpFormatter, argument_default = SUPPRESS)
parser.add_argument("aaSpecs",
metavar = "AA_SPEC",
nargs = "*",
help = "Archive specification. It determines the archive specification file that shall "
"be processed. If AA_SPEC contains the \".aa\" extension then it is taken as the "
"path to an archive specification file. Otherwise, if specified without the "
"extension, the corresponding .aa file is searched in the archive specifications "
f"directory (see option --{Options.ARCHIVE_SPECS_DIR}).")
# {{{ commands
commandsGroup = parser.add_argument_group(
"Commands",
"Commands for program's operations. The default operation is the backup " +
"creation if no command is specified.")
commandsGroup.add_argument(cls.__makeCmdlineOption(CmdlineCommands.LIST),
action = "store_true",
help = "Show configured and orphaned archives.")
commandsGroup.add_argument(cls.__makeCmdlineOption(CmdlineCommands.PURGE),
action = "store_true",
help = "Purge stored data for an orphaned archive.")
commandsGroup.add_argument("--version",
action = "version",
version = version,
help = "Show program's version number and exit.")
commandsGroup.add_argument("-h", "--help", action = "help", help = "Show this help message and exit.")
# }}} commands
# {{{ archiving related options
archivingGroup = parser.add_argument_group("Archiving options")
archiverChoices = [OptionsUtils.archiverTypeToStr(arch) for arch in ArchiverTypes]
archivingGroup.add_argument("-a", cls.__makeCmdlineOption(Options.ARCHIVER),
metavar = "ARCHIVER",
choices = archiverChoices,
help = f"Specify archiver type. Supported types are: {archiverChoices} "
"(default: targz).")
compressionLevelChoices = [str(level) for level in range(0, 10)]
archivingGroup.add_argument("-c", cls.__makeCmdlineOption(Options.COMPRESSION_LEVEL),
choices = compressionLevelChoices,
metavar = "NUM",
help = "Compression strength level. If not specified, default "
"behaviour of underlying compression program will be used. "
f"Valid range is from {compressionLevelChoices[0]} to "
f"{compressionLevelChoices[-1]}.")
archivingGroup.add_argument("-d", cls.__makeCmdlineOption(Options.DEST_DIR),
metavar = "DIR_PATH",
help = "Directory where the backup will be created (default: <current directory>).")
archivingGroup.add_argument(cls.__makeCmdlineOption(Options.OVERWRITE_AT_START),
action = "store_true",
help = "If enabled, backups are overwritten at the start of creation. If "
"disabled (default), backups are overwritten at the end of creation. "
"Enabling this option can be useful with big backups and low free space "
"on the backup volume.")
# }}} archiving related options
# {{{ incremental archiving related options
incrementalGroup = parser.add_argument_group("Incremental archiving options")
incrementalGroup.add_argument("-i", cls.__makeCmdlineOption(Options.INCREMENTAL),
action = "store_true",
help = "Perform incremental backup.")
incrementalGroup.add_argument("-l", cls.__makeCmdlineOption(Options.LEVEL),
type = int,
help = "Specify the backup level which should be created. All information "
"about higher levels---if any exists---will be erased. If not "
"present, the next level in a row will be created.")
incrementalGroup.add_argument(cls.__makeCmdlineOption(Options.RESTARTING),
action = "store_true",
help = "Turns on backup level restarting. See other '*restart-*' options to "
"configure the restarting behaviour.")
incrementalGroup.add_argument(cls.__makeCmdlineOption(Options.RESTART_AFTER_LEVEL),
type = int,
metavar = "LEVEL",
help = "Maximal backup level. If reached, it will be restarted back to a lower "
"level (which is typically level 1 but it depends "
f"on '--{Options.MAX_RESTART_LEVEL_SIZE}') (default: 10).")
incrementalGroup.add_argument(cls.__makeCmdlineOption(Options.RESTART_AFTER_AGE),
type = int,
metavar = "DAYS",
help = "Number of days after which the backup level is restarted. Similarly "
f"to '--{Options.RESTART_AFTER_LEVEL}' it will be restarted to level 1 "
"or higher.")
incrementalGroup.add_argument(cls.__makeCmdlineOption(Options.FULL_RESTART_AFTER_COUNT),
type = int,
metavar = "COUNT",
help = "Number of backup level restarts after which the level is restarted to 0.")
incrementalGroup.add_argument(cls.__makeCmdlineOption(Options.FULL_RESTART_AFTER_AGE),
type = int,
metavar = "DAYS",
help = "Number of days after which the backup level is restarted to 0.")
incrementalGroup.add_argument(cls.__makeCmdlineOption(Options.MAX_RESTART_LEVEL_SIZE),
type = int,
metavar = "PERCENTAGE",
help = "Maximal percentage size of a backup (of level > 0) to which level is " +
"allowed restart to. The size is percentage of size of the level 0 " +
"backup file. If a backup of particular level has its size bigger " +
"than defined percentage, restart to that level will not be allowed.")
incrementalGroup.add_argument(cls.__makeCmdlineOption(Options.REMOVE_OBSOLETE_BACKUPS),
action = "store_true",
help = "Turn on removing backups of levels that are no longer valid due to " +
"the backup level restart. All backups of the backup level higher " +
"than the one currently being created will be removed.")
# }}} incremental archiving related options
# {{{ options related to old backups keeping
keepingGroup = parser.add_argument_group("Options for keeping old backups")
keepingGroup.add_argument("-k", cls.__makeCmdlineOption(Options.KEEP_OLD_BACKUPS),
action = "store_true",
help = "Turn on backup keeping. When a backup is about to be overwritten, it is "
f"renamed instead. If '--{Options.INCREMENTAL}' is enabled it applies to "
"all corresponding increments. The new name is created by inserting a "
"keeping ID in front of backup file(s) extension. The keeping ID is a "
"string from interval 'aa', 'ab', ..., 'zy', 'zz' where 'aa' represents "
"most recent kept backup.")
keepingGroup.add_argument(cls.__makeCmdlineOption(Options.NUMBER_OF_OLD_BACKUPS),
type = int,
metavar = "NUM",
help = f"Number of old backups to keep when '--{Options.KEEP_OLD_BACKUPS}' is "
"enabled (default: 1).")
# }}} options related to old backups keeping
# {{{ command execution options
commandExecutionGroup = parser.add_argument_group("Command execution options")
commandExecutionGroup.add_argument(cls.__makeCmdlineOption(Options.COMMAND_BEFORE_ALL_BACKUPS),
type = str,
metavar = "COMMAND_BEFORE_ALL",
help = "Arbitrary command that will be executed before backup creation " +
"for the set of selected archives.")
commandExecutionGroup.add_argument(cls.__makeCmdlineOption(Options.COMMAND_AFTER_ALL_BACKUPS),
metavar = "COMMAND_AFTER_ALL",
help = "Arbitrary command that will be executed after backup creation " +
"for the set of selected archives.")
commandExecutionGroup.add_argument(cls.__makeCmdlineOption(Options.COMMAND_BEFORE_BACKUP),
metavar = "COMMAND_BEFORE",
help = "Arbitrary command to execute prior to each backup creation.")
commandExecutionGroup.add_argument(cls.__makeCmdlineOption(Options.COMMAND_AFTER_BACKUP),
metavar = "COMMAND_AFTER",
help = "Arbitrary command to execute after each backup creation.")
# }}} command execution options
# {{{ general options
generalGroup = parser.add_argument_group("General options")
generalGroup.add_argument("-v", cls.__makeCmdlineOption(Options.VERBOSE),
action = "count",
help = "Turn on verbose output.")
generalGroup.add_argument("-q", cls.__makeCmdlineOption(Options.QUIET),
action = "store_true",
help = f"Turn on quiet output. Only errors will be shown. If --{Options.QUIET} "
f"is turned on at the same level as --{Options.VERBOSE} (e. g. both are "
f"specified on the command line) then --{Options.QUIET} has higher "
f"priority than --{Options.VERBOSE}.")
generalGroup.add_argument(cls.__makeCmdlineOption(Options.ALL),
action = "store_true",
help = "Operate on all configured archives (additional to those specified as " +
f"AA_SPEC arguments). Default for --{CmdlineCommands.LIST} if no AA_SPEC " +
f"is specified. See also --{Options.ARCHIVE_SPECS_DIR}.")
generalGroup.add_argument(cls.__makeCmdlineOption(Options.ARCHIVE_SPECS_DIR),
metavar = "DIR_PATH",
help = "Directory where archive specification files will be searched for (default: " +
"~/.config/aa/archive_specs).")
generalGroup.add_argument(cls.__makeCmdlineOption(Options.USER_CONFIG_FILE),
metavar = "FILE_PATH",
help = "Alternate user configuration file (default: ~/.config/aa/aa.conf).")
generalGroup.add_argument(cls.__makeCmdlineOption(Options.USER_CONFIG_DIR),
metavar = "DIR_PATH",
help = "Alternate user configuration directory (default: ~/.config/aa).")
# }}} general options
# {{{ force options
forceGroup = parser.add_argument_group("Force options", "Options to override standard options defined in " +
"archive specification files.")
forceGroup.add_argument(cls.__makeCmdlineOption(Options.FORCE_ARCHIVER),
choices = archiverChoices,
metavar = "ARCHIVER",
help = f"Force archiver type. See --{Options.ARCHIVER} option for supported types.")
forceGroup.add_argument(cls.__makeCmdlineOption(Options.FORCE_INCREMENTAL),
action = "store_true",
help = "Force incremental backup.")
forceGroup.add_argument(cls.__makeCmdlineOption(Options.FORCE_RESTARTING),
action = "store_true",
help = "Force backup level restarting.")
forceGroup.add_argument(cls.__makeCmdlineOption(Options.FORCE_COMPRESSION_LEVEL),
choices = compressionLevelChoices,
metavar = "NUM",
help = "Force compression strength level.")
forceGroup.add_argument(cls.__makeCmdlineOption(Options.FORCE_DEST_DIR),
metavar = "DIR_PATH",
help = "Force the directory where the backup will be created.")
forceGroup.add_argument(cls.__makeCmdlineOption(Options.FORCE_COMMAND_BEFORE_BACKUP),
metavar = "COMMAND_BEFORE",
help = "Force configuration of the command to execute prior to each backup creation.")
forceGroup.add_argument(cls.__makeCmdlineOption(Options.FORCE_COMMAND_AFTER_BACKUP),
metavar = "COMMAND_AFTER",
help = "Force configuration of the command to execute after each backup creation.")
forceGroup.add_argument(cls.__makeCmdlineOption(Options.FORCE_OVERWRITE_AT_START),
action = "store_true",
help = "Force backup overwriting behavior.")
# }}} force options
# {{{ negation options
negationGroup = parser.add_argument_group("Negation options", "Negative variants of standard boolean options.")
negationGroup.add_argument(cls.__makeCmdlineOption(Options.NO_INCREMENTAL),
action = "store_true",
help = "Disable incremental backup.")
negationGroup.add_argument(cls.__makeCmdlineOption(Options.NO_RESTARTING),
action = "store_true",
help = "Turn off backup level restarting.")
negationGroup.add_argument(cls.__makeCmdlineOption(Options.NO_REMOVE_OBSOLETE_BACKUPS),
action = "store_true",
help = "Turn off obsolete backups removing.")
negationGroup.add_argument(cls.__makeCmdlineOption(Options.NO_KEEP_OLD_BACKUPS),
action = "store_true",
help = "Turn off backup keeping.")
negationGroup.add_argument(cls.__makeCmdlineOption(Options.NO_ALL),
action = "store_true",
help = "Do not operate on all configured archive specification files.")
negationGroup.add_argument(cls.__makeCmdlineOption(Options.NO_OVERWRITE_AT_START),
action = "store_true",
help = "Do not overwrite backup at the start of creation. Overwrite after the new " +
"backup is created.")
# }}} negation options
args = parser.parse_args(programArgs)
options = vars(args)
arguments = options["aaSpecs"] if "aaSpecs" in options else []
return options, arguments
@staticmethod
def __makeCmdlineOption(option):
return "--" + str(option)
# }}} CLASSES