mirror of
https://github.com/YunoHost/yunohost.git
synced 2024-09-03 20:06:10 +02:00
[fix] Hook backup method
This commit is contained in:
parent
ffc0c8d22e
commit
1b25e123ae
8 changed files with 250 additions and 255 deletions
|
@ -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",
|
||||
|
|
|
@ -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/<name>/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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"""
|
||||
|
||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
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)
|
||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
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)
|
||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
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):
|
||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
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:
|
||||
|
|
Loading…
Add table
Reference in a new issue