Source code for wasp_backup.command_common

# -*- coding: utf-8 -*-
# wasp_backup/command_common.py
#
# Copyright (C) 2017 the wasp-backup authors and contributors
# <see AUTHORS file>
#
# This file is part of wasp-backup.
#
# wasp-backup is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# wasp-backup 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with wasp-backup.  If not, see <http://www.gnu.org/licenses/>.

# TODO: document the code
# TODO: write tests for the code

# noinspection PyUnresolvedReferences
from wasp_backup.version import __author__, __version__, __credits__, __license__, __copyright__, __email__
# noinspection PyUnresolvedReferences
from wasp_backup.version import __status__

import os
import sys
import shlex
from datetime import datetime
import tempfile

from wasp_general.verify import verify_type
from wasp_general.uri import WURI
from wasp_general.network.clients.proto import WNetworkClientProto
from wasp_general.network.clients.base import WCommonNetworkClientCapability
from wasp_general.network.clients.collection import __default_client_collection__
from wasp_general.command.enhanced import WCommandArgumentDescriptor
from wasp_general.crypto.aes import WAESMode
from wasp_general.command.result import WPlainCommandResult
from wasp_general.command.enhanced import WEnhancedCommand

from wasp_backup.core import WBackupMeta, WBackupMetaProvider
from wasp_backup.notify import notify


[docs]class WCompressionArgumentHelper(WCommandArgumentDescriptor.ArgumentCastingHelper): def __init__(self): WCommandArgumentDescriptor.ArgumentCastingHelper.__init__( self, casting_fn=self.cast_string )
[docs] @staticmethod @verify_type(value=str) def cast_string(value): value = value.lower() if value == 'gzip': return WBackupMeta.Archive.CompressionMode.gzip elif value == 'bzip2': return WBackupMeta.Archive.CompressionMode.bzip2 elif value == 'disabled': return else: raise ValueError('Invalid compression value')
[docs]def cipher_name_validation(cipher_name): try: if WAESMode.parse_cipher_name(cipher_name) is not None: return True except ValueError: pass return False
__common_args__ = { 'backup-archive': WCommandArgumentDescriptor( 'backup-archive', required=True, multiple_values=False, meta_var='archive_path', help_info='backup file path' ), 'input-files': WCommandArgumentDescriptor( 'input-files', required=True, multiple_values=True, meta_var='input_path', help_info='files or directories to backup' ), 'input-program': WCommandArgumentDescriptor( 'input-program', required=True, multiple_values=False, meta_var='program_command', help_info='program which output will be backed up' ), 'compression': WCommandArgumentDescriptor( 'compression', meta_var='compression_type', help_info='compression option. One of: "gzip", "bzip2" or "disabled". It is disabled by default', casting_helper=WCompressionArgumentHelper() ), 'password': WCommandArgumentDescriptor( 'password', meta_var='encryption_password', help_info='password to encrypt backup. Backup is not encrypted by default' ), 'cipher_algorithm': WCommandArgumentDescriptor( 'cipher_algorithm', meta_var='algorithm_name', help_info='cipher that will be used for encrypt (backup will not be encrypted if password was not ' 'set). It is "AES-256-CBC" by default', casting_helper=WCommandArgumentDescriptor.StringArgumentCastingHelper( validate_fn=cipher_name_validation ), default_value='AES-256-CBC' ), 'io-write-rate': WCommandArgumentDescriptor( 'io-write-rate', meta_var='maximum writing rate', help_info='use this parameter to limit disk I/O load (bytes per second). You can use ' 'suffixes like "K" for kibibytes, "M" for mebibytes, "G" for gibibytes, "T" for tebibytes for ' 'convenience ', casting_helper=WCommandArgumentDescriptor.DataSizeArgumentHelper() ), 'copy-to': WCommandArgumentDescriptor( 'copy-to', meta_var='URL', help_info='Location to copy backup archive to' ), 'copy-fail': WCommandArgumentDescriptor( 'copy-fail', flag_mode=True, help_info='If specified, then backup will fail if copy operation fails. ' '(But local archive would not be deleted any way)', ), 'notify-app': WCommandArgumentDescriptor( 'notify-app', meta_var='app_path', help_info='Application that will be called as a handler' ), } # noinspection PyAbstractClass
[docs]class WBackupCommand(WEnhancedCommand): __command__ = None __arguments__ = tuple() __relationships__ = None def __init__(self, logger): WEnhancedCommand.__init__( self, self.__command__, *self.__arguments__, relationships=self.__relationships__ ) self.__logger = logger self.__stop_event = None
[docs] def logger(self): return self.__logger
[docs] def stop_event(self, value=None): if value is not None: self.__stop_event = value return self.__stop_event
# noinspection PyAbstractClass
[docs]class WCreateBackupCommand(WBackupCommand):
[docs] class UploadFailed(Exception): pass
def __init__(self, logger): WBackupCommand.__init__(self, logger) self.__archiver = None
[docs] def archiver(self): return self.__archiver
[docs] def set_archiver(self, value): self.__archiver = value
def _create_backup(self, command_arguments, *args, **kwargs): archiver = self.archiver() if archiver is None: raise RuntimeError('Archiver must be set before call') try: backup_started_at = datetime.utcnow() archiver.archive(*args, **kwargs) backup_duration = (datetime.utcnow() - backup_started_at).seconds copy_to = None if 'copy-to' in command_arguments.keys(): copy_to = command_arguments['copy-to'] notify_app = None if 'notify-app' in command_arguments.keys(): notify_app = command_arguments['notify-app'] copy_fail = False if 'copy-fail' in command_arguments.keys(): copy_fail = command_arguments['copy-fail'] if copy_to is None: return self.__handle_backup_result( 'Archive "%s" was created successfully' % archiver.archive_path(), notify_app=notify_app, backup_duration=backup_duration ) copy_result, copy_duration = self.__copy(archiver.archive_path(), copy_to) if copy_result is True: backup_result = \ 'Archive "%s" was created and uploaded successfully' % archiver.archive_path() elif copy_fail is False: backup_result = \ 'Archive "%s" was created successfully. But it fails to upload archive to ' \ 'destination' % archiver.archive_path() else: raise WCreateBackupCommand.UploadFailed( 'Unable to upload archive "%s"' % archiver.archive_path() ) return self.__handle_backup_result( backup_result, notify_app=notify_app, backup_duration=backup_duration, copy_to=copy_to, copy_complete=copy_result, copy_duration=copy_duration ) finally: self.set_archiver(None) def __copy(self, archive_path, copy_to): try: copy_started_at = datetime.utcnow() uri = WURI.parse(copy_to) if uri.path() is not None: dir_name, file_name = os.path.split(uri.path()) uri.component(WURI.Component.path, dir_name) network_client = __default_client_collection__.open(uri) with open(archive_path, 'rb') as f: copy_result = network_client.request(WCommonNetworkClientCapability.upload_file, file_name, f) copy_duration = (datetime.utcnow() - copy_started_at).total_seconds() return copy_result, copy_duration except WNetworkClientProto.ConnectionError: pass return False, -1 def __handle_backup_result( self, str_result, notify_app=None, backup_duration=None, copy_to=None, copy_complete=None, copy_duration=None ): archiver = self.archiver() if archiver is None: raise RuntimeError('Archiver must be set before call') if notify_app is None: return WPlainCommandResult(str_result) meta_data = archiver.meta() meta_data[WBackupMeta.BackupNotificationOptions.created_archive] = archiver.archive_path() meta_data[WBackupMeta.BackupNotificationOptions.backup_duration] = backup_duration meta_data[WBackupMeta.BackupNotificationOptions.total_archive_size] = os.stat(archiver.archive_path()).st_size meta_data[WBackupMeta.BackupNotificationOptions.copy_to] = copy_to meta_data[WBackupMeta.BackupNotificationOptions.copy_completion] = copy_complete meta_data[WBackupMeta.BackupNotificationOptions.copy_duration] = copy_duration notify( meta_data, notify_app, encode_strict_cls=(WBackupMeta.Archive.MetaOptions, WBackupMeta.BackupNotificationOptions) ) return WPlainCommandResult(str_result)