AutoArchive.starter — AutoArchive Developer Manual (ver. 2.1.0.dev20260309)

# Source code for AutoArchive.starter

    # starter.py
    #
    # Project: AutoArchive
    # License: GNU GPLv3
    #
    # Copyright (C) 2003 - 2026 Róbert Čerňanský

    __all__ = ["Starter"]

    import os
    import sys
    from abc import ABCMeta, abstractmethod
    from argparse import ArgumentParser, RawDescriptionHelpFormatter, SUPPRESS, Namespace

    from AutoArchive._application.archiving_application import ArchivingApplication
    from AutoArchive._infrastructure._app_environment import AppEnvironment
    from AutoArchive._infrastructure._application_context import ApplicationContext
    from AutoArchive._infrastructure.configuration import Constants as ConfigurationConstants
    from AutoArchive._infrastructure.configuration import Options, OptionsUtils, ArchiverTypes
    from AutoArchive._infrastructure.configuration._configuration_factory import ConfigurationFactory
    from AutoArchive._infrastructure.service._service_accessor import ServiceAccessor
    from AutoArchive._infrastructure.storage._file_storage import FileStorage
    from AutoArchive._infrastructure.utils import Constants, Utils, ErrorHandling, OsPaths
    from AutoArchive._services.archiver._archiver_service_component import ArchiverServiceComponent
    from AutoArchive._services.external_command_executor._external_command_executor_service_component import \
        ExternalCommandExecutorServiceComponent
    from AutoArchive._ui.cmdline._cmdline_commands import CmdlineCommands
    from AutoArchive._ui.cmdline._cmdline_ui import CmdlineUi
    from AutoArchive._ui.cmdline._user_action_executor import UserActionExecutor
    from ._infrastructure.configuration._directory_creator import DirectoryCreator
    from ._meta import _Meta
    from ._update.update import Update

    #: Tuple of service components
    _SERVICE_COMPONENTS = (ExternalCommandExecutorServiceComponent, ArchiverServiceComponent)

    class Starter(metaclass = ABCMeta):
        "Fires up the show."

        @abstractmethod
        def __init__(self):
            pass

        @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)

                tempConfiguration = ConfigurationFactory.makeConfiguration(appEnvironment)
                Update(tempConfiguration, CmdlineUi(appEnvironment, tempConfiguration)).migrate()

                configuration = ConfigurationFactory.makeConfiguration(appEnvironment)

                DirectoryCreator(configuration).createUserConfigSpace()

                storage = FileStorage(configuration, OsPaths.getUserDataDir())

                applicationContext = ApplicationContext(appEnvironment, configuration, storage)
                serviceAccessor = ServiceAccessor(OsPaths.getUserDataDir())
                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:
                    ErrorHandling.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(cls.__makeCmdlineOption(CmdlineCommands.LIST_AVAILABLE_ARCHIVERS),
                                       action = "store_true",
                                       help = "List archiver types that are available in current installation.")

            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]
            archiverChoicesStr = str.join(", ", [f"'{ac}'" for ac in archiverChoices])
            archivingGroup.add_argument("-a", cls.__makeCmdlineOption(Options.ARCHIVER),
                                        metavar = "ARCHIVER",
                                        choices = archiverChoices,
                                        help = f"Specify archiver type.  Supported types are: {archiverChoicesStr} "
                                               "(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: " +
                                             f"<{Options.USER_CONFIG_DIR}>{os.path.sep}archive_specs).")

            generalGroup.add_argument(cls.__makeCmdlineOption(Options.USER_CONFIG_FILE),
                                      metavar = "FILE_PATH",
                                      help = f"User configuration file (default: <{Options.USER_CONFIG_DIR}>" +
                                             f"{os.path.sep}{ConfigurationConstants.CONFIG_FILE_NAME}).")

            userConfigDirDefault = r"%%USERPROFILE%%\AutoArchive or %%HOMEDRIVE%%\%%HOMEPATH%%\AutoArchive if not set." \
                if Utils.isWindows() else "${XDG_CONFIG_HOME}/aa or ~/.config/aa if not set."
            generalGroup.add_argument(cls.__makeCmdlineOption(Options.USER_CONFIG_DIR),
                                      metavar = "DIR_PATH",
                                      help = f"User configuration directory (default: {userConfigDirDefault}).")

            # }}} 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: Namespace = parser.parse_args(programArgs)
            optionsWithUnderscores: dict[str, str] = vars(args)
            arguments = optionsWithUnderscores["aaSpecs"] if "aaSpecs" in optionsWithUnderscores else []
            optionsWithDashes = {optionName.replace("_", "-"): optionsWithUnderscores[optionName]
                                 for optionName in optionsWithUnderscores}
            return optionsWithDashes, arguments

        @staticmethod
        def __makeCmdlineOption(option):
            return "--" + str(option)