From 1b25e123ae0f132c0bf4fb41db0cd0d9743ea6f0 Mon Sep 17 00:00:00 2001 From: ljf Date: Fri, 28 Oct 2022 09:36:54 +0200 Subject: [PATCH] [fix] Hook backup method --- locales/en.json | 6 +- share/actionsmap.yml | 26 +++- share/config_backup_repository.toml | 1 - src/backup.py | 34 ++--- src/repositories/borg.py | 55 ++++--- src/repositories/hook.py | 213 ++++++++++++---------------- src/repositories/tar.py | 56 ++++---- src/repository.py | 114 ++++++++------- 8 files changed, 250 insertions(+), 255 deletions(-) diff --git a/locales/en.json b/locales/en.json index 3121c3e51..1a04b2ba3 100644 --- a/locales/en.json +++ b/locales/en.json @@ -88,9 +88,9 @@ "backup_archive_system_part_not_available": "System part '{part}' unavailable in this backup", "backup_archive_writing_error": "Could not add the files '{source}' (named in the archive '{dest}') to be backed up into the compressed archive '{archive}'", "backup_ask_for_copying_if_needed": "Do you want to perform the backup using {size}MB temporarily? (This way is used since some files could not be prepared using a more efficient method.)", - "backup_borg_init_error": "Unable initialize the borg repository: {error}", - "backup_borg_list_archive_error": "Unable to list files in the archive", - "backup_borg_mount_archive_error": "Unable to mount the archive here: {error}", + "backup_init_error": "Unable initialize the borg repository: {error}", + "backup_list_archives_names_error": "Unable to list files in the archive", + "backup_mount_error": "Unable to mount the archive here: {error}", "backup_cant_mount_uncompress_archive": "Could not mount the uncompressed archive as write protected", "backup_cleaning_failed": "Could not clean up the temporary backup folder", "backup_copying_to_organize_the_archive": "Copying {size}MB to organize the archive", diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 7e004ab76..e03d2aaa2 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -1104,6 +1104,10 @@ backup: full: --repositories help: List archives in these repositories nargs: "*" + -p: + full: --prefix + help: List only archives starting with this prefix + default: "" -i: full: --with-info help: Show backup information for each archive @@ -1144,6 +1148,27 @@ backup: full: --repository help: Repository in which we can found the archive + ### backup_extract() + extract: + action_help: Extract data from a backup archive + api: POST /backups//extract + arguments: + name: + help: Name of the backup archive + paths: + help: Path to extract + nargs: "*" + -r: + full: --repository + help: Repository in which we can found the archive + -t: + full: --target + help: Destination path for extracted data + -e: + full: --exclude + help: Paths to exclude + nargs: "*" + ### backup_mount() mount: action_help: Mount a backup archive if possible @@ -1286,7 +1311,6 @@ backup: pattern: *pattern_backup_repository_shortname --prefix: help: Prefix on which we prune - nargs: "?" -H: full: --keep-hourly help: Number of hourly archives to keep diff --git a/share/config_backup_repository.toml b/share/config_backup_repository.toml index cc0c5290f..3ab3cd271 100644 --- a/share/config_backup_repository.toml +++ b/share/config_backup_repository.toml @@ -80,7 +80,6 @@ name.en = "" choices.borg = "BorgBackup (recommended)" choices.tar = "Legacy tar archive mechanism" default = "borg" - visible = "!is_remote" [main.main.path] type = "path" diff --git a/src/backup.py b/src/backup.py index b470bf163..b81b2b9a8 100644 --- a/src/backup.py +++ b/src/backup.py @@ -56,7 +56,7 @@ from yunohost.tools import ( ) from yunohost.regenconf import regen_conf from yunohost.log import OperationLogger, is_unit_operation -from yunohost.repository import BackupRepository, BackupArchive +from yunohost.repository import BackupRepository, BackupArchive, BACKUP_PATH from yunohost.utils.config import ConfigPanel from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.utils.system import ( @@ -67,8 +67,6 @@ from yunohost.utils.system import ( ) from yunohost.settings import settings_get -BACKUP_PATH = "/home/yunohost.backup" -ARCHIVES_PATH = f"{BACKUP_PATH}/archives" APP_MARGIN_SPACE_SIZE = 100 # In MB CONF_MARGIN_SPACE_SIZE = 10 # IN MB POSTINSTALL_ESTIMATE_SPACE_SIZE = 5 # In MB @@ -1860,7 +1858,7 @@ def backup_restore(name, repository=None, system=[], apps=[], force=False): return restore_manager.targets.results -def backup_list(name=None, repositories=[], with_info=False, human_readable=False): +def backup_list(name=None, repositories=[], prefix="", with_info=False, human_readable=False): """ List available local backup archives @@ -1879,7 +1877,7 @@ def backup_list(name=None, repositories=[], with_info=False, human_readable=Fals return archive.list(with_info) return { - name: BackupRepository(name).list_archives(with_info) + name: BackupRepository(name).list_archives(with_info=with_info, prefix=prefix) for name in repositories or BackupRepository.list(full=False) } @@ -1895,6 +1893,20 @@ def backup_download(name, repository=None): return archive.download() +def backup_extract(name, paths, repository=None, target=None, exclude=[]): + + if not repository: + repository = settings_get("misc.backup.backup_default_repositories")[0] + + if not target: + target = os.getcwd() + + repo = BackupRepository(repository) + archive = BackupArchive(repo, name) + + archive.extract(path=paths, target=target, exclude_paths=exclude) + + def backup_mount(name, path, repository=None): if not repository: @@ -2237,18 +2249,6 @@ def backup_timer_pause(operation_logger, shortname): # -def _create_archive_dir(): - """Create the YunoHost archives directory if doesn't exist""" - - if not os.path.isdir(ARCHIVES_PATH): - if os.path.lexists(ARCHIVES_PATH): - raise YunohostError("backup_output_symlink_dir_broken", path=ARCHIVES_PATH) - - # Create the archive folder, with 'admins' as groupowner, such that - # people can scp archives out of the server - mkdir(ARCHIVES_PATH, mode=0o770, parents=True, gid="admins") - - def _call_for_each_path(self, callback, csv_path=None): """Call a callback for each path in csv""" diff --git a/src/repositories/borg.py b/src/repositories/borg.py index 7c0fc4d9c..5ec61b1c4 100644 --- a/src/repositories/borg.py +++ b/src/repositories/borg.py @@ -1,23 +1,21 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2013 Yunohost - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program 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 Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import subprocess import json @@ -126,7 +124,6 @@ class BorgBackupRepository(LocalBackupRepository): if "quota" in self.future_values and self.future_values["quota"]: cmd += ['--storage-quota', self.quota] - logger.debug(cmd) try: self._call('init', cmd) except YunohostError as e: @@ -182,11 +179,11 @@ class BorgBackupArchive(BackupArchive): def backup(self): cmd = ['borg', 'create', self.archive_path, './'] - self.repo._call('backup', cmd, cwd=self.work_dir) + self.repository._call('backup', cmd, cwd=self.work_dir) def delete(self): cmd = ['borg', 'delete', '--force', self.archive_path] - self.repo._call('delete_archive', cmd) + self.repository._call('delete_archive', cmd) def list(self, with_info=False): """ Return a list of archives names @@ -196,7 +193,7 @@ class BorgBackupArchive(BackupArchive): """ cmd = ["borg", "list", "--json-lines" if with_info else "--short", self.archive_path] - out = self.repo._call('list_archive', cmd) + out = self.repository._call('list_archive', cmd) if not with_info: return out.decode() @@ -218,7 +215,7 @@ class BorgBackupArchive(BackupArchive): cmd = ["borg", "export-tar", self.archive_path, "-"] + paths for path in exclude_paths: cmd += ['--exclude', path] - reader = self.repo._run_borg_command(cmd, stdout=subprocess.PIPE) + reader = self.repository._run_borg_command(cmd, stdout=subprocess.PIPE) # We return a raw bottle HTTPresponse (instead of serializable data like # list/dict, ...), which is gonna be picked and used directly by moulinette @@ -226,15 +223,15 @@ class BorgBackupArchive(BackupArchive): response.content_type = "application/x-tar" return HTTPResponse(reader, 200) - def extract(self, paths=[], destination=None, exclude_paths=[]): + def extract(self, paths=[], target=None, exclude_paths=[]): # TODO exclude_paths not available in actions map - paths, destination, exclude_paths = super().extract(paths, destination, exclude_paths) + paths, target, exclude_paths = super().extract(paths, target, exclude_paths) cmd = ['borg', 'extract', self.archive_path] + paths for path in exclude_paths: cmd += ['--exclude', path] - return self.repo._call('extract_archive', cmd, cwd=destination) + return self.repository._call('extract_archive', cmd, cwd=target) def mount(self, path): # FIXME How to be sure the place where we mount is secure ? cmd = ['borg', 'mount', self.archive_path, path] - self.repo._call('mount_archive', cmd) + self.repository._call('mount_archive', cmd) diff --git a/src/repositories/hook.py b/src/repositories/hook.py index 7641b05f9..af74593b8 100644 --- a/src/repositories/hook.py +++ b/src/repositories/hook.py @@ -1,158 +1,121 @@ -# -*- coding: utf-8 -*- +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +import json -""" License - - Copyright (C) 2013 Yunohost - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program 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 Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" from moulinette import m18n from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import rm -from yunohost.hook import hook_callback +from yunohost.hook import hook_callback, hook_exec from yunohost.utils.error import YunohostError -from yunohost.repository import BackupRepository, BackupArchive +from yunohost.repository import LocalBackupRepository, BackupArchive, HOOK_METHOD_DIR logger = getActionLogger("yunohost.repository") -class HookBackupRepository(BackupRepository): +def hook_backup_call(self, action, chdir=None, **kwargs): + """ Call a submethod of backup method hook + + Exceptions: + backup_custom_ACTION_error -- Raised if the custom script failed + """ + if isinstance(self, LocalBackupRepository): + repository = self + args = [action, "", "", repository.location, "", repository.description] + else: + repository = self.repository + args = [action, self.work_dir, self.name, repository.location, "", ""] + args += kwargs.values() + + env = {"YNH_BACKUP_" + key.upper(): str(value) + for key, value in list(kwargs.items()) + list(repository.values.items())} + + return_code, return_data = hook_exec( + f"{HOOK_METHOD_DIR}/{repository.method}", + args=args, + raise_on_error=False, + chdir=chdir, + env=env, + ) + + if return_code == 38: + raise NotImplementedError() + elif return_code != 0: + raise YunohostError("backup_" + action + "_error") + + return return_code, return_data + + +class HookBackupRepository(LocalBackupRepository): method_name = "hook" # ================================================= # Repository actions # ================================================= def install(self): - raise NotImplementedError() + _, return_data = hook_backup_call(self, "install") + if return_data.get("super"): + super().install() def update(self): - raise NotImplementedError() + hook_backup_call(self, "update") - def remove(self, purge=False): - if self.__class__ == BackupRepository: - raise NotImplementedError() # purge + def purge(self): + _, return_data = hook_backup_call(self, "purge") + if return_data.get("super"): + super().purge() - rm(self.save_path, force=True) - logger.success(m18n.n("repository_removed", repository=self.shortname)) - - def list(self): - raise NotImplementedError() - - def info(self, space_used=False): - result = super().get(mode="export") - - if self.__class__ == BackupRepository and space_used is True: - raise NotImplementedError() # purge - - return {self.shortname: result} - - def prune(self): - raise NotImplementedError() + def list_archives_names(self, prefix=""): + _, return_data = hook_backup_call(self, "list_archives_names", + prefix=prefix) + return return_data class HookBackupArchive(BackupArchive): # ================================================= # Archive actions # ================================================= + def need_organized_files(self): + return_code, _ = hook_backup_call(self, "need_mount") + return int(return_code) == 0 + def backup(self): - raise NotImplementedError() - """ - Launch a custom script to backup - """ - - self._call('backup', self.work_dir, self.name, self.repo.location, self.manager.size, - self.manager.description) - - def restore(self): - raise NotImplementedError() + hook_backup_call(self, "backup") + #self._call('backup', self.work_dir, self.name, self.repo.location, self.manager.size, self.manager.description) def delete(self): - raise NotImplementedError() + hook_backup_call(self, "delete") - def list(self): - raise NotImplementedError() - """ Return a list of archives names + def list(self, with_info=False): + return_code, return_data = hook_backup_call(self, "list_files", + with_info=with_info) - Exceptions: - backup_custom_list_error -- Raised if the custom script failed - """ - out = self._call('list', self.repo.location) - result = out.strip().splitlines() - return result + return return_data - def info(self): - raise NotImplementedError() # compute_space_used - """ Return json string of the info.json file + def download(self, exclude_paths=[]): + hook_backup_call(self, "download") - Exceptions: - backup_custom_info_error -- Raised if the custom script failed - """ - return self._call('info', self.name, self.repo.location) + def extract(self, paths=[], target=None, exclude_paths=[]): + paths, destination, exclude_paths = super().extract(paths, target, exclude_paths) + hook_backup_call(self, "extract", + chdir=target, + paths=",".join(paths), + exclude=",".join(exclude_paths)) - def download(self): - raise NotImplementedError() - - def mount(self): - raise NotImplementedError() - """ - Launch a custom script to mount the custom archive - """ - super().mount() - self._call('mount', self.work_dir, self.name, self.repo.location, self.manager.size, - self.manager.description) - - def extract(self): - raise NotImplementedError() - - def need_organized_files(self): - """Call the backup_method hook to know if we need to organize files""" - if self._need_mount is not None: - return self._need_mount - - try: - self._call('nedd_mount') - except YunohostError: - return False - return True - - def _call(self, *args): - """ Call a submethod of backup method hook - - Exceptions: - backup_custom_ACTION_error -- Raised if the custom script failed - """ - ret = hook_callback("backup_method", [self.method], - args=args) - - ret_failed = [ - hook - for hook, infos in ret.items() - if any(result["state"] == "failed" for result in infos.values()) - ] - if ret_failed: - raise YunohostError("backup_custom_" + args[0] + "_error") - - return ret["succeed"][self.method]["stdreturn"] - - def _get_args(self, action): - """Return the arguments to give to the custom script""" - return [ - action, - self.work_dir, - self.name, - self.repo, - self.manager.size, - self.manager.description, - ] + def mount(self, path): + hook_backup_call(self, "mount", path=path) diff --git a/src/repositories/tar.py b/src/repositories/tar.py index b676d5844..65fa02528 100644 --- a/src/repositories/tar.py +++ b/src/repositories/tar.py @@ -1,23 +1,21 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2013 Yunohost - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program 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 Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import os import tarfile import shutil @@ -81,9 +79,9 @@ class TarBackupArchive(BackupArchive): if isinstance(self.manager, BackupManager) and settings_get( "backup.compress_tar_archives" ): - return os.path.join(self.repo.location, self.name + ".tar.gz") + return os.path.join(self.repository.location, self.name + ".tar.gz") - f = os.path.join(self.repo.path, self.name + ".tar") + f = os.path.join(self.repository.path, self.name + ".tar") if os.path.exists(f + ".gz"): f += ".gz" return f @@ -124,12 +122,12 @@ class TarBackupArchive(BackupArchive): # Move info file shutil.copy( os.path.join(self.work_dir, "info.json"), - os.path.join(self.repo.location, self.name + ".info.json"), + os.path.join(self.repository.location, self.name + ".info.json"), ) # If backuped to a non-default location, keep a symlink of the archive # to that location - link = os.path.join(self.repo.path, self.name + ".tar") + link = os.path.join(self.repository.path, self.name + ".tar") if not os.path.isfile(link): os.symlink(self.archive_path, link) @@ -144,8 +142,8 @@ class TarBackupArchive(BackupArchive): tar.close() def delete(self): - archive_file = f"{self.repo.location}/{self.name}.tar" - info_file = f"{self.repo.location}/{self.name}.info.json" + archive_file = f"{self.repository.location}/{self.name}.tar" + info_file = f"{self.repository.location}/{self.name}.info.json" if os.path.exists(archive_file + ".gz"): archive_file += ".gz" @@ -219,8 +217,8 @@ class TarBackupArchive(BackupArchive): archive_folder, archive_file_name = archive_file.rsplit("/", 1) return static_file(archive_file_name, archive_folder, download=archive_file_name) - def extract(self, paths=[], destination=None, exclude_paths=[]): - paths, destination, exclude_paths = super().extract(paths, destination, exclude_paths) + def extract(self, paths=[], target=None, exclude_paths=[]): + paths, target, exclude_paths = super().extract(paths, target, exclude_paths) # Open the tarball try: tar = tarfile.open( @@ -241,7 +239,7 @@ class TarBackupArchive(BackupArchive): and all([not tarinfo.name.startswith(path) for path in exclude_paths]) ) ] - tar.extractall(members=subdir_and_files, path=destination) + tar.extractall(members=subdir_and_files, path=target) tar.close() def mount(self, path): diff --git a/src/repository.py b/src/repository.py index 55c19a245..617037f4f 100644 --- a/src/repository.py +++ b/src/repository.py @@ -1,28 +1,21 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2013 Yunohost - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program 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 Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" - -""" yunohost_repository.py - - Manage backup repositories -""" +# +# Copyright (c) 2022 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# import json import os import re @@ -47,7 +40,10 @@ from yunohost.utils.system import disk_usage, binary_to_human from yunohost.utils.network import get_ssh_public_key, SHF_BASE_URL logger = getActionLogger('yunohost.repository') +BACKUP_PATH = "/home/yunohost.backup" +ARCHIVES_PATH = f"{BACKUP_PATH}/archives" REPOSITORIES_DIR = '/etc/yunohost/backup/repositories' +HOOK_METHOD_DIR = '/etc/yunohost/hooks.d/backup_method' CACHE_INFO_DIR = "/var/cache/yunohost/repositories/{repository}" REPOSITORY_CONFIG_PATH = "/usr/share/yunohost/other/config_repository.toml" MB_ALLOWED_TO_ORGANIZE = 10 @@ -159,21 +155,35 @@ class BackupRepository(ConfigPanel): logger.debug("SHF running") return {'is_shf': True} - def post_ask__is_remote(self, question): - if question.value: - self.method = 'borg' - self._cast_by_backup_method() - return {} - def post_ask__method(self, question): if question.value: - self.method = question.value + self.new_values["method"] = question.value self._cast_by_backup_method() return {} # ================================================= # Config Panel Override # ================================================= + def _get_toml(self): + + toml = super()._get_toml() + + # Add custom methods to the list of choices + methods = toml["main"]["main"]["method"]["choices"] + + try: + hook_methods = os.listdir(HOOK_METHOD_DIR) + except FileNotFoundError: + hook_methods = [] + + for hook_method in hook_methods: + methods[hook_method] = hook_method.replace('-', ' ').replace('_', ' ').capitalize() + + # Change default path + toml["main"]["main"]["path"]["default"] = f"{ARCHIVES_PATH}/{self.entity}" + + return toml + def _get_default_values(self): values = super()._get_default_values() # TODO move that in a getter hooks ? @@ -268,8 +278,8 @@ class BackupRepository(ConfigPanel): return {self.entity: result} - def list_archives(self, with_info=False): - archives = self.list_archives_names() + def list_archives(self, with_info=False, prefix=""): + archives = self.list_archives_names(prefix=prefix) if with_info: d = {} for archive in archives: @@ -373,7 +383,7 @@ class BackupRepository(ConfigPanel): def purge(self): raise NotImplementedError() - def list_archives_names(self, prefix=None): + def list_archives_names(self, prefix=""): raise NotImplementedError() def compute_space_used(self): @@ -386,7 +396,11 @@ class BackupRepository(ConfigPanel): class LocalBackupRepository(BackupRepository): def install(self): self.new_values['location'] = self.location - mkdir(self.location, mode=0o0750, parents=True, uid="admin", gid="root", force=True) + if not os.path.isdir(self.location): + if os.path.lexists(self.location): + raise YunohostError("backup_output_symlink_dir_broken", path=self.location) + + mkdir(self.location, mode=0o0750, parents=True, gid="admins", force=True) def update(self): self.install() @@ -404,24 +418,24 @@ class BackupArchive: self.name = self.name[: -len(".tar.gz")] elif self.name.endswith(".tar"): self.name = self.name[: -len(".tar")] - self.repo = repo + self.repository = repo # Cast - if self.repo.method_name == 'tar': + if self.repository.method_name == 'tar': self.__class__ = yunohost.repositories.tar.TarBackupArchive - elif self.repo.method_name == 'borg': + elif self.repository.method_name == 'borg': self.__class__ = yunohost.repositories.borg.BorgBackupArchive else: self.__class__ = yunohost.repositories.hook.HookBackupArchive # Assert archive exists - if self.manager.__class__.__name__ != "BackupManager" and self.name not in self.repo.list_archives(False): + if self.manager.__class__.__name__ != "BackupManager" and self.name not in self.repository.list_archives(False): raise YunohostValidationError("backup_archive_name_unknown", name=name) @property def archive_path(self): """Return the archive path""" - return self.repo.location + '::' + self.name + return self.repository.location + '::' + self.name @property def work_dir(self): @@ -434,11 +448,11 @@ class BackupArchive: For a RestoreManager, it is the directory where we mount the archive before restoring """ - return self.manager.work_dir + return self.manager.work_dir if self.manager else None # This is not a property cause it could be managed in a hook def need_organized_files(self): - return self.repo.need_organized_files + return self.repository.need_organized_files def organize_and_backup(self): """ @@ -450,10 +464,10 @@ class BackupArchive: if self.need_organized_files(): self._organize_files() - self.repo.install() + self.repository.install() # Check free space in output - self.repo.check_is_enough_free_space(self.manager.size) + self.repository.check_is_enough_free_space(self.manager.size) try: self.backup() finally: @@ -506,7 +520,7 @@ class BackupArchive: def _get_info_string(self): """Extract info file from archive if needed and read it""" - cache_info_dir = CACHE_INFO_DIR.format(repository=self.repo.entity) + cache_info_dir = CACHE_INFO_DIR.format(repository=self.repository.entity) mkdir(cache_info_dir, mode=0o0700, parents=True, force=True) info_file = f"{cache_info_dir}/{self.name}.info.json" @@ -515,9 +529,9 @@ class BackupArchive: try: files_in_archive = self.list() if "info.json" in files_in_archive: - self.extract("info.json", destination=tmp_dir) + self.extract("info.json", target=tmp_dir) elif "./info.json" in files_in_archive: - self.extract("./info.json", destination=tmp_dir) + self.extract("./info.json", target=tmp_dir) else: raise YunohostError( "backup_archive_cant_retrieve_info_json", archive=self.archive_path @@ -741,7 +755,7 @@ class BackupArchive: ) return - def extract(self, paths=None, destination=None, exclude_paths=[]): + def extract(self, paths=None, target=None, exclude_paths=[]): if self.__class__ == BackupArchive: raise NotImplementedError() if isinstance(paths, str): @@ -750,7 +764,7 @@ class BackupArchive: paths = self.select_files() if isinstance(exclude_paths, str): exclude_paths = [exclude_paths] - return paths, destination, exclude_paths + return paths, target, exclude_paths def mount(self): if self.__class__ == BackupArchive: