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_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_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_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_init_error": "Unable initialize the borg repository: {error}",
|
||||||
"backup_borg_list_archive_error": "Unable to list files in the archive",
|
"backup_list_archives_names_error": "Unable to list files in the archive",
|
||||||
"backup_borg_mount_archive_error": "Unable to mount the archive here: {error}",
|
"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_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_cleaning_failed": "Could not clean up the temporary backup folder",
|
||||||
"backup_copying_to_organize_the_archive": "Copying {size}MB to organize the archive",
|
"backup_copying_to_organize_the_archive": "Copying {size}MB to organize the archive",
|
||||||
|
|
|
@ -1104,6 +1104,10 @@ backup:
|
||||||
full: --repositories
|
full: --repositories
|
||||||
help: List archives in these repositories
|
help: List archives in these repositories
|
||||||
nargs: "*"
|
nargs: "*"
|
||||||
|
-p:
|
||||||
|
full: --prefix
|
||||||
|
help: List only archives starting with this prefix
|
||||||
|
default: ""
|
||||||
-i:
|
-i:
|
||||||
full: --with-info
|
full: --with-info
|
||||||
help: Show backup information for each archive
|
help: Show backup information for each archive
|
||||||
|
@ -1144,6 +1148,27 @@ backup:
|
||||||
full: --repository
|
full: --repository
|
||||||
help: Repository in which we can found the archive
|
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()
|
### backup_mount()
|
||||||
mount:
|
mount:
|
||||||
action_help: Mount a backup archive if possible
|
action_help: Mount a backup archive if possible
|
||||||
|
@ -1286,7 +1311,6 @@ backup:
|
||||||
pattern: *pattern_backup_repository_shortname
|
pattern: *pattern_backup_repository_shortname
|
||||||
--prefix:
|
--prefix:
|
||||||
help: Prefix on which we prune
|
help: Prefix on which we prune
|
||||||
nargs: "?"
|
|
||||||
-H:
|
-H:
|
||||||
full: --keep-hourly
|
full: --keep-hourly
|
||||||
help: Number of hourly archives to keep
|
help: Number of hourly archives to keep
|
||||||
|
|
|
@ -80,7 +80,6 @@ name.en = ""
|
||||||
choices.borg = "BorgBackup (recommended)"
|
choices.borg = "BorgBackup (recommended)"
|
||||||
choices.tar = "Legacy tar archive mechanism"
|
choices.tar = "Legacy tar archive mechanism"
|
||||||
default = "borg"
|
default = "borg"
|
||||||
visible = "!is_remote"
|
|
||||||
|
|
||||||
[main.main.path]
|
[main.main.path]
|
||||||
type = "path"
|
type = "path"
|
||||||
|
|
|
@ -56,7 +56,7 @@ from yunohost.tools import (
|
||||||
)
|
)
|
||||||
from yunohost.regenconf import regen_conf
|
from yunohost.regenconf import regen_conf
|
||||||
from yunohost.log import OperationLogger, is_unit_operation
|
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.config import ConfigPanel
|
||||||
from yunohost.utils.error import YunohostError, YunohostValidationError
|
from yunohost.utils.error import YunohostError, YunohostValidationError
|
||||||
from yunohost.utils.system import (
|
from yunohost.utils.system import (
|
||||||
|
@ -67,8 +67,6 @@ from yunohost.utils.system import (
|
||||||
)
|
)
|
||||||
from yunohost.settings import settings_get
|
from yunohost.settings import settings_get
|
||||||
|
|
||||||
BACKUP_PATH = "/home/yunohost.backup"
|
|
||||||
ARCHIVES_PATH = f"{BACKUP_PATH}/archives"
|
|
||||||
APP_MARGIN_SPACE_SIZE = 100 # In MB
|
APP_MARGIN_SPACE_SIZE = 100 # In MB
|
||||||
CONF_MARGIN_SPACE_SIZE = 10 # IN MB
|
CONF_MARGIN_SPACE_SIZE = 10 # IN MB
|
||||||
POSTINSTALL_ESTIMATE_SPACE_SIZE = 5 # 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
|
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
|
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 archive.list(with_info)
|
||||||
|
|
||||||
return {
|
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)
|
for name in repositories or BackupRepository.list(full=False)
|
||||||
}
|
}
|
||||||
|
@ -1895,6 +1893,20 @@ def backup_download(name, repository=None):
|
||||||
return archive.download()
|
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):
|
def backup_mount(name, path, repository=None):
|
||||||
|
|
||||||
if not repository:
|
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):
|
def _call_for_each_path(self, callback, csv_path=None):
|
||||||
"""Call a callback for each path in csv"""
|
"""Call a callback for each path in csv"""
|
||||||
|
|
||||||
|
|
|
@ -1,23 +1,21 @@
|
||||||
# -*- coding: utf-8 -*-
|
#
|
||||||
|
# Copyright (c) 2022 YunoHost Contributors
|
||||||
""" License
|
#
|
||||||
|
# This file is part of YunoHost (see https://yunohost.org)
|
||||||
Copyright (C) 2013 Yunohost
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
This program is free software; you can redistribute it and/or modify
|
# it under the terms of the GNU Affero General Public License as
|
||||||
it under the terms of the GNU Affero General Public License as published
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
by the Free Software Foundation, either version 3 of the License, or
|
# License, or (at your option) any later version.
|
||||||
(at your option) any later version.
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
This program is distributed in the hope that it will be useful,
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
# GNU Affero General Public License for more details.
|
||||||
GNU Affero General Public License for more details.
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
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/>.
|
||||||
along with this program; if not, see http://www.gnu.org/licenses
|
#
|
||||||
|
|
||||||
"""
|
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import json
|
import json
|
||||||
|
@ -126,7 +124,6 @@ class BorgBackupRepository(LocalBackupRepository):
|
||||||
if "quota" in self.future_values and self.future_values["quota"]:
|
if "quota" in self.future_values and self.future_values["quota"]:
|
||||||
cmd += ['--storage-quota', self.quota]
|
cmd += ['--storage-quota', self.quota]
|
||||||
|
|
||||||
logger.debug(cmd)
|
|
||||||
try:
|
try:
|
||||||
self._call('init', cmd)
|
self._call('init', cmd)
|
||||||
except YunohostError as e:
|
except YunohostError as e:
|
||||||
|
@ -182,11 +179,11 @@ class BorgBackupArchive(BackupArchive):
|
||||||
|
|
||||||
def backup(self):
|
def backup(self):
|
||||||
cmd = ['borg', 'create', self.archive_path, './']
|
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):
|
def delete(self):
|
||||||
cmd = ['borg', 'delete', '--force', self.archive_path]
|
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):
|
def list(self, with_info=False):
|
||||||
""" Return a list of archives names
|
""" Return a list of archives names
|
||||||
|
@ -196,7 +193,7 @@ class BorgBackupArchive(BackupArchive):
|
||||||
"""
|
"""
|
||||||
cmd = ["borg", "list", "--json-lines" if with_info else "--short",
|
cmd = ["borg", "list", "--json-lines" if with_info else "--short",
|
||||||
self.archive_path]
|
self.archive_path]
|
||||||
out = self.repo._call('list_archive', cmd)
|
out = self.repository._call('list_archive', cmd)
|
||||||
|
|
||||||
if not with_info:
|
if not with_info:
|
||||||
return out.decode()
|
return out.decode()
|
||||||
|
@ -218,7 +215,7 @@ class BorgBackupArchive(BackupArchive):
|
||||||
cmd = ["borg", "export-tar", self.archive_path, "-"] + paths
|
cmd = ["borg", "export-tar", self.archive_path, "-"] + paths
|
||||||
for path in exclude_paths:
|
for path in exclude_paths:
|
||||||
cmd += ['--exclude', path]
|
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
|
# We return a raw bottle HTTPresponse (instead of serializable data like
|
||||||
# list/dict, ...), which is gonna be picked and used directly by moulinette
|
# 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"
|
response.content_type = "application/x-tar"
|
||||||
return HTTPResponse(reader, 200)
|
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
|
# 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
|
cmd = ['borg', 'extract', self.archive_path] + paths
|
||||||
for path in exclude_paths:
|
for path in exclude_paths:
|
||||||
cmd += ['--exclude', path]
|
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):
|
def mount(self, path):
|
||||||
# FIXME How to be sure the place where we mount is secure ?
|
# FIXME How to be sure the place where we mount is secure ?
|
||||||
cmd = ['borg', 'mount', self.archive_path, path]
|
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 import m18n
|
||||||
from moulinette.utils.log import getActionLogger
|
from moulinette.utils.log import getActionLogger
|
||||||
from moulinette.utils.filesystem import rm
|
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.utils.error import YunohostError
|
||||||
from yunohost.repository import BackupRepository, BackupArchive
|
from yunohost.repository import LocalBackupRepository, BackupArchive, HOOK_METHOD_DIR
|
||||||
logger = getActionLogger("yunohost.repository")
|
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"
|
method_name = "hook"
|
||||||
|
|
||||||
# =================================================
|
# =================================================
|
||||||
# Repository actions
|
# Repository actions
|
||||||
# =================================================
|
# =================================================
|
||||||
def install(self):
|
def install(self):
|
||||||
raise NotImplementedError()
|
_, return_data = hook_backup_call(self, "install")
|
||||||
|
if return_data.get("super"):
|
||||||
|
super().install()
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
raise NotImplementedError()
|
hook_backup_call(self, "update")
|
||||||
|
|
||||||
def remove(self, purge=False):
|
def purge(self):
|
||||||
if self.__class__ == BackupRepository:
|
_, return_data = hook_backup_call(self, "purge")
|
||||||
raise NotImplementedError() # purge
|
if return_data.get("super"):
|
||||||
|
super().purge()
|
||||||
|
|
||||||
rm(self.save_path, force=True)
|
def list_archives_names(self, prefix=""):
|
||||||
logger.success(m18n.n("repository_removed", repository=self.shortname))
|
_, return_data = hook_backup_call(self, "list_archives_names",
|
||||||
|
prefix=prefix)
|
||||||
def list(self):
|
return return_data
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
class HookBackupArchive(BackupArchive):
|
class HookBackupArchive(BackupArchive):
|
||||||
# =================================================
|
# =================================================
|
||||||
# Archive actions
|
# Archive actions
|
||||||
# =================================================
|
# =================================================
|
||||||
|
def need_organized_files(self):
|
||||||
|
return_code, _ = hook_backup_call(self, "need_mount")
|
||||||
|
return int(return_code) == 0
|
||||||
|
|
||||||
def backup(self):
|
def backup(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)
|
||||||
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()
|
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
raise NotImplementedError()
|
hook_backup_call(self, "delete")
|
||||||
|
|
||||||
def list(self):
|
def list(self, with_info=False):
|
||||||
raise NotImplementedError()
|
return_code, return_data = hook_backup_call(self, "list_files",
|
||||||
""" Return a list of archives names
|
with_info=with_info)
|
||||||
|
|
||||||
Exceptions:
|
return return_data
|
||||||
backup_custom_list_error -- Raised if the custom script failed
|
|
||||||
"""
|
|
||||||
out = self._call('list', self.repo.location)
|
|
||||||
result = out.strip().splitlines()
|
|
||||||
return result
|
|
||||||
|
|
||||||
def info(self):
|
def download(self, exclude_paths=[]):
|
||||||
raise NotImplementedError() # compute_space_used
|
hook_backup_call(self, "download")
|
||||||
""" Return json string of the info.json file
|
|
||||||
|
|
||||||
Exceptions:
|
def extract(self, paths=[], target=None, exclude_paths=[]):
|
||||||
backup_custom_info_error -- Raised if the custom script failed
|
paths, destination, exclude_paths = super().extract(paths, target, exclude_paths)
|
||||||
"""
|
hook_backup_call(self, "extract",
|
||||||
return self._call('info', self.name, self.repo.location)
|
chdir=target,
|
||||||
|
paths=",".join(paths),
|
||||||
|
exclude=",".join(exclude_paths))
|
||||||
|
|
||||||
def download(self):
|
def mount(self, path):
|
||||||
raise NotImplementedError()
|
hook_backup_call(self, "mount", path=path)
|
||||||
|
|
||||||
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,
|
|
||||||
]
|
|
||||||
|
|
|
@ -1,23 +1,21 @@
|
||||||
# -*- coding: utf-8 -*-
|
#
|
||||||
|
# Copyright (c) 2022 YunoHost Contributors
|
||||||
""" License
|
#
|
||||||
|
# This file is part of YunoHost (see https://yunohost.org)
|
||||||
Copyright (C) 2013 Yunohost
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
This program is free software; you can redistribute it and/or modify
|
# it under the terms of the GNU Affero General Public License as
|
||||||
it under the terms of the GNU Affero General Public License as published
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
by the Free Software Foundation, either version 3 of the License, or
|
# License, or (at your option) any later version.
|
||||||
(at your option) any later version.
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
This program is distributed in the hope that it will be useful,
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
# GNU Affero General Public License for more details.
|
||||||
GNU Affero General Public License for more details.
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
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/>.
|
||||||
along with this program; if not, see http://www.gnu.org/licenses
|
#
|
||||||
|
|
||||||
"""
|
|
||||||
import os
|
import os
|
||||||
import tarfile
|
import tarfile
|
||||||
import shutil
|
import shutil
|
||||||
|
@ -81,9 +79,9 @@ class TarBackupArchive(BackupArchive):
|
||||||
if isinstance(self.manager, BackupManager) and settings_get(
|
if isinstance(self.manager, BackupManager) and settings_get(
|
||||||
"backup.compress_tar_archives"
|
"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"):
|
if os.path.exists(f + ".gz"):
|
||||||
f += ".gz"
|
f += ".gz"
|
||||||
return f
|
return f
|
||||||
|
@ -124,12 +122,12 @@ class TarBackupArchive(BackupArchive):
|
||||||
# Move info file
|
# Move info file
|
||||||
shutil.copy(
|
shutil.copy(
|
||||||
os.path.join(self.work_dir, "info.json"),
|
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
|
# If backuped to a non-default location, keep a symlink of the archive
|
||||||
# to that location
|
# 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):
|
if not os.path.isfile(link):
|
||||||
os.symlink(self.archive_path, link)
|
os.symlink(self.archive_path, link)
|
||||||
|
|
||||||
|
@ -144,8 +142,8 @@ class TarBackupArchive(BackupArchive):
|
||||||
tar.close()
|
tar.close()
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
archive_file = f"{self.repo.location}/{self.name}.tar"
|
archive_file = f"{self.repository.location}/{self.name}.tar"
|
||||||
info_file = f"{self.repo.location}/{self.name}.info.json"
|
info_file = f"{self.repository.location}/{self.name}.info.json"
|
||||||
if os.path.exists(archive_file + ".gz"):
|
if os.path.exists(archive_file + ".gz"):
|
||||||
archive_file += ".gz"
|
archive_file += ".gz"
|
||||||
|
|
||||||
|
@ -219,8 +217,8 @@ class TarBackupArchive(BackupArchive):
|
||||||
archive_folder, archive_file_name = archive_file.rsplit("/", 1)
|
archive_folder, archive_file_name = archive_file.rsplit("/", 1)
|
||||||
return static_file(archive_file_name, archive_folder, download=archive_file_name)
|
return static_file(archive_file_name, archive_folder, download=archive_file_name)
|
||||||
|
|
||||||
def extract(self, paths=[], destination=None, exclude_paths=[]):
|
def extract(self, paths=[], target=None, exclude_paths=[]):
|
||||||
paths, destination, exclude_paths = super().extract(paths, destination, exclude_paths)
|
paths, target, exclude_paths = super().extract(paths, target, exclude_paths)
|
||||||
# Open the tarball
|
# Open the tarball
|
||||||
try:
|
try:
|
||||||
tar = tarfile.open(
|
tar = tarfile.open(
|
||||||
|
@ -241,7 +239,7 @@ class TarBackupArchive(BackupArchive):
|
||||||
and all([not tarinfo.name.startswith(path) for path in exclude_paths])
|
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()
|
tar.close()
|
||||||
|
|
||||||
def mount(self, path):
|
def mount(self, path):
|
||||||
|
|
|
@ -1,28 +1,21 @@
|
||||||
# -*- coding: utf-8 -*-
|
#
|
||||||
|
# Copyright (c) 2022 YunoHost Contributors
|
||||||
""" License
|
#
|
||||||
|
# This file is part of YunoHost (see https://yunohost.org)
|
||||||
Copyright (C) 2013 Yunohost
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
This program is free software; you can redistribute it and/or modify
|
# it under the terms of the GNU Affero General Public License as
|
||||||
it under the terms of the GNU Affero General Public License as published
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
by the Free Software Foundation, either version 3 of the License, or
|
# License, or (at your option) any later version.
|
||||||
(at your option) any later version.
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
This program is distributed in the hope that it will be useful,
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
# GNU Affero General Public License for more details.
|
||||||
GNU Affero General Public License for more details.
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
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/>.
|
||||||
along with this program; if not, see http://www.gnu.org/licenses
|
#
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
""" yunohost_repository.py
|
|
||||||
|
|
||||||
Manage backup repositories
|
|
||||||
"""
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
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
|
from yunohost.utils.network import get_ssh_public_key, SHF_BASE_URL
|
||||||
|
|
||||||
logger = getActionLogger('yunohost.repository')
|
logger = getActionLogger('yunohost.repository')
|
||||||
|
BACKUP_PATH = "/home/yunohost.backup"
|
||||||
|
ARCHIVES_PATH = f"{BACKUP_PATH}/archives"
|
||||||
REPOSITORIES_DIR = '/etc/yunohost/backup/repositories'
|
REPOSITORIES_DIR = '/etc/yunohost/backup/repositories'
|
||||||
|
HOOK_METHOD_DIR = '/etc/yunohost/hooks.d/backup_method'
|
||||||
CACHE_INFO_DIR = "/var/cache/yunohost/repositories/{repository}"
|
CACHE_INFO_DIR = "/var/cache/yunohost/repositories/{repository}"
|
||||||
REPOSITORY_CONFIG_PATH = "/usr/share/yunohost/other/config_repository.toml"
|
REPOSITORY_CONFIG_PATH = "/usr/share/yunohost/other/config_repository.toml"
|
||||||
MB_ALLOWED_TO_ORGANIZE = 10
|
MB_ALLOWED_TO_ORGANIZE = 10
|
||||||
|
@ -159,21 +155,35 @@ class BackupRepository(ConfigPanel):
|
||||||
logger.debug("SHF running")
|
logger.debug("SHF running")
|
||||||
return {'is_shf': True}
|
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):
|
def post_ask__method(self, question):
|
||||||
if question.value:
|
if question.value:
|
||||||
self.method = question.value
|
self.new_values["method"] = question.value
|
||||||
self._cast_by_backup_method()
|
self._cast_by_backup_method()
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
# =================================================
|
# =================================================
|
||||||
# Config Panel Override
|
# 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):
|
def _get_default_values(self):
|
||||||
values = super()._get_default_values()
|
values = super()._get_default_values()
|
||||||
# TODO move that in a getter hooks ?
|
# TODO move that in a getter hooks ?
|
||||||
|
@ -268,8 +278,8 @@ class BackupRepository(ConfigPanel):
|
||||||
|
|
||||||
return {self.entity: result}
|
return {self.entity: result}
|
||||||
|
|
||||||
def list_archives(self, with_info=False):
|
def list_archives(self, with_info=False, prefix=""):
|
||||||
archives = self.list_archives_names()
|
archives = self.list_archives_names(prefix=prefix)
|
||||||
if with_info:
|
if with_info:
|
||||||
d = {}
|
d = {}
|
||||||
for archive in archives:
|
for archive in archives:
|
||||||
|
@ -373,7 +383,7 @@ class BackupRepository(ConfigPanel):
|
||||||
def purge(self):
|
def purge(self):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def list_archives_names(self, prefix=None):
|
def list_archives_names(self, prefix=""):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def compute_space_used(self):
|
def compute_space_used(self):
|
||||||
|
@ -386,7 +396,11 @@ class BackupRepository(ConfigPanel):
|
||||||
class LocalBackupRepository(BackupRepository):
|
class LocalBackupRepository(BackupRepository):
|
||||||
def install(self):
|
def install(self):
|
||||||
self.new_values['location'] = self.location
|
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):
|
def update(self):
|
||||||
self.install()
|
self.install()
|
||||||
|
@ -404,24 +418,24 @@ class BackupArchive:
|
||||||
self.name = self.name[: -len(".tar.gz")]
|
self.name = self.name[: -len(".tar.gz")]
|
||||||
elif self.name.endswith(".tar"):
|
elif self.name.endswith(".tar"):
|
||||||
self.name = self.name[: -len(".tar")]
|
self.name = self.name[: -len(".tar")]
|
||||||
self.repo = repo
|
self.repository = repo
|
||||||
|
|
||||||
# Cast
|
# Cast
|
||||||
if self.repo.method_name == 'tar':
|
if self.repository.method_name == 'tar':
|
||||||
self.__class__ = yunohost.repositories.tar.TarBackupArchive
|
self.__class__ = yunohost.repositories.tar.TarBackupArchive
|
||||||
elif self.repo.method_name == 'borg':
|
elif self.repository.method_name == 'borg':
|
||||||
self.__class__ = yunohost.repositories.borg.BorgBackupArchive
|
self.__class__ = yunohost.repositories.borg.BorgBackupArchive
|
||||||
else:
|
else:
|
||||||
self.__class__ = yunohost.repositories.hook.HookBackupArchive
|
self.__class__ = yunohost.repositories.hook.HookBackupArchive
|
||||||
|
|
||||||
# Assert archive exists
|
# 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)
|
raise YunohostValidationError("backup_archive_name_unknown", name=name)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def archive_path(self):
|
def archive_path(self):
|
||||||
"""Return the archive path"""
|
"""Return the archive path"""
|
||||||
return self.repo.location + '::' + self.name
|
return self.repository.location + '::' + self.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def work_dir(self):
|
def work_dir(self):
|
||||||
|
@ -434,11 +448,11 @@ class BackupArchive:
|
||||||
For a RestoreManager, it is the directory where we mount the archive
|
For a RestoreManager, it is the directory where we mount the archive
|
||||||
before restoring
|
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
|
# This is not a property cause it could be managed in a hook
|
||||||
def need_organized_files(self):
|
def need_organized_files(self):
|
||||||
return self.repo.need_organized_files
|
return self.repository.need_organized_files
|
||||||
|
|
||||||
def organize_and_backup(self):
|
def organize_and_backup(self):
|
||||||
"""
|
"""
|
||||||
|
@ -450,10 +464,10 @@ class BackupArchive:
|
||||||
if self.need_organized_files():
|
if self.need_organized_files():
|
||||||
self._organize_files()
|
self._organize_files()
|
||||||
|
|
||||||
self.repo.install()
|
self.repository.install()
|
||||||
|
|
||||||
# Check free space in output
|
# 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:
|
try:
|
||||||
self.backup()
|
self.backup()
|
||||||
finally:
|
finally:
|
||||||
|
@ -506,7 +520,7 @@ class BackupArchive:
|
||||||
def _get_info_string(self):
|
def _get_info_string(self):
|
||||||
"""Extract info file from archive if needed and read it"""
|
"""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)
|
mkdir(cache_info_dir, mode=0o0700, parents=True, force=True)
|
||||||
info_file = f"{cache_info_dir}/{self.name}.info.json"
|
info_file = f"{cache_info_dir}/{self.name}.info.json"
|
||||||
|
|
||||||
|
@ -515,9 +529,9 @@ class BackupArchive:
|
||||||
try:
|
try:
|
||||||
files_in_archive = self.list()
|
files_in_archive = self.list()
|
||||||
if "info.json" in files_in_archive:
|
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:
|
elif "./info.json" in files_in_archive:
|
||||||
self.extract("./info.json", destination=tmp_dir)
|
self.extract("./info.json", target=tmp_dir)
|
||||||
else:
|
else:
|
||||||
raise YunohostError(
|
raise YunohostError(
|
||||||
"backup_archive_cant_retrieve_info_json", archive=self.archive_path
|
"backup_archive_cant_retrieve_info_json", archive=self.archive_path
|
||||||
|
@ -741,7 +755,7 @@ class BackupArchive:
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
def extract(self, paths=None, destination=None, exclude_paths=[]):
|
def extract(self, paths=None, target=None, exclude_paths=[]):
|
||||||
if self.__class__ == BackupArchive:
|
if self.__class__ == BackupArchive:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
if isinstance(paths, str):
|
if isinstance(paths, str):
|
||||||
|
@ -750,7 +764,7 @@ class BackupArchive:
|
||||||
paths = self.select_files()
|
paths = self.select_files()
|
||||||
if isinstance(exclude_paths, str):
|
if isinstance(exclude_paths, str):
|
||||||
exclude_paths = [exclude_paths]
|
exclude_paths = [exclude_paths]
|
||||||
return paths, destination, exclude_paths
|
return paths, target, exclude_paths
|
||||||
|
|
||||||
def mount(self):
|
def mount(self):
|
||||||
if self.__class__ == BackupArchive:
|
if self.__class__ == BackupArchive:
|
||||||
|
|
Loading…
Add table
Reference in a new issue