mirror of
https://github.com/YunoHost/yunohost.git
synced 2024-09-03 20:06:10 +02:00
[wip] Support local repository
This commit is contained in:
parent
48f7e51dd8
commit
a7af79d93e
2 changed files with 123 additions and 121 deletions
|
@ -24,7 +24,6 @@
|
||||||
Manage backups
|
Manage backups
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
import tarfile
|
import tarfile
|
||||||
|
@ -52,6 +51,7 @@ from yunohost.monitor import binary_to_human
|
||||||
from yunohost.tools import tools_postinstall
|
from yunohost.tools import tools_postinstall
|
||||||
from yunohost.service import service_regen_conf
|
from yunohost.service import service_regen_conf
|
||||||
from yunohost.log import OperationLogger
|
from yunohost.log import OperationLogger
|
||||||
|
from yunohost.repository import BackupRepository
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
|
||||||
BACKUP_PATH = '/home/yunohost.backup'
|
BACKUP_PATH = '/home/yunohost.backup'
|
||||||
|
@ -1425,7 +1425,9 @@ class BackupMethod(object):
|
||||||
BackupRepository object. If None, the default repo is used :
|
BackupRepository object. If None, the default repo is used :
|
||||||
/home/yunohost.backup/archives/
|
/home/yunohost.backup/archives/
|
||||||
"""
|
"""
|
||||||
self.repo = ARCHIVES_PATH if repo is None else repo
|
if not repo or isinstance(repo, basestring):
|
||||||
|
repo = BackupRepository.get_or_create(ARCHIVES_PATH)
|
||||||
|
self.repo = repo
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def method_name(self):
|
def method_name(self):
|
||||||
|
@ -1596,7 +1598,7 @@ class BackupMethod(object):
|
||||||
try:
|
try:
|
||||||
subprocess.check_call(["mount", "--rbind", src, dest])
|
subprocess.check_call(["mount", "--rbind", src, dest])
|
||||||
subprocess.check_call(["mount", "-o", "remount,ro,bind", dest])
|
subprocess.check_call(["mount", "-o", "remount,ro,bind", dest])
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.warning(m18n.n("backup_couldnt_bind", src=src, dest=dest))
|
logger.warning(m18n.n("backup_couldnt_bind", src=src, dest=dest))
|
||||||
# To check if dest is mounted, use /proc/mounts that
|
# To check if dest is mounted, use /proc/mounts that
|
||||||
# escape spaces as \040
|
# escape spaces as \040
|
||||||
|
@ -1697,6 +1699,7 @@ class CopyBackupMethod(BackupMethod):
|
||||||
|
|
||||||
def __init__(self, repo=None):
|
def __init__(self, repo=None):
|
||||||
super(CopyBackupMethod, self).__init__(repo)
|
super(CopyBackupMethod, self).__init__(repo)
|
||||||
|
filesystem.mkdir(self.repo.path, parent=True)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def method_name(self):
|
def method_name(self):
|
||||||
|
@ -1709,7 +1712,7 @@ class CopyBackupMethod(BackupMethod):
|
||||||
|
|
||||||
for path in self.manager.paths_to_backup:
|
for path in self.manager.paths_to_backup:
|
||||||
source = path['source']
|
source = path['source']
|
||||||
dest = os.path.join(self.repo, path['dest'])
|
dest = os.path.join(self.repo.path, path['dest'])
|
||||||
if source == dest:
|
if source == dest:
|
||||||
logger.debug("Files already copyed")
|
logger.debug("Files already copyed")
|
||||||
return
|
return
|
||||||
|
@ -1735,18 +1738,18 @@ class CopyBackupMethod(BackupMethod):
|
||||||
# the ynh cli
|
# the ynh cli
|
||||||
super(CopyBackupMethod, self).mount()
|
super(CopyBackupMethod, self).mount()
|
||||||
|
|
||||||
if not os.path.isdir(self.repo):
|
if not os.path.isdir(self.repo.path):
|
||||||
raise YunohostError('backup_no_uncompress_archive_dir')
|
raise YunohostError('backup_no_uncompress_archive_dir')
|
||||||
|
|
||||||
filesystem.mkdir(self.work_dir, parent=True)
|
filesystem.mkdir(self.work_dir, parent=True)
|
||||||
ret = subprocess.call(["mount", "-r", "--rbind", self.repo,
|
ret = subprocess.call(["mount", "-r", "--rbind", self.repo.path,
|
||||||
self.work_dir])
|
self.work_dir])
|
||||||
if ret == 0:
|
if ret == 0:
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
logger.warning(m18n.n("bind_mouting_disable"))
|
logger.warning(m18n.n("bind_mouting_disable"))
|
||||||
subprocess.call(["mountpoint", "-q", dest,
|
subprocess.call(["mountpoint", "-q", self.repo.path,
|
||||||
"&&", "umount", "-R", dest])
|
"&&", "umount", "-R", self.repo.path])
|
||||||
raise YunohostError('backup_cant_mount_uncompress_archive')
|
raise YunohostError('backup_cant_mount_uncompress_archive')
|
||||||
|
|
||||||
|
|
||||||
|
@ -1758,6 +1761,7 @@ class TarBackupMethod(BackupMethod):
|
||||||
|
|
||||||
def __init__(self, repo=None):
|
def __init__(self, repo=None):
|
||||||
super(TarBackupMethod, self).__init__(repo)
|
super(TarBackupMethod, self).__init__(repo)
|
||||||
|
filesystem.mkdir(self.repo.path, parent=True)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def method_name(self):
|
def method_name(self):
|
||||||
|
@ -1766,7 +1770,7 @@ class TarBackupMethod(BackupMethod):
|
||||||
@property
|
@property
|
||||||
def _archive_file(self):
|
def _archive_file(self):
|
||||||
"""Return the compress archive path"""
|
"""Return the compress archive path"""
|
||||||
return os.path.join(self.repo, self.name + '.tar.gz')
|
return os.path.join(self.repo.path, self.name + '.tar.gz')
|
||||||
|
|
||||||
def backup(self):
|
def backup(self):
|
||||||
"""
|
"""
|
||||||
|
@ -1781,8 +1785,8 @@ class TarBackupMethod(BackupMethod):
|
||||||
compress archive
|
compress archive
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not os.path.exists(self.repo):
|
if not os.path.exists(self.repo.path):
|
||||||
filesystem.mkdir(self.repo, 0o750, parents=True, uid='admin')
|
filesystem.mkdir(self.repo.path, 0o750, parents=True, uid='admin')
|
||||||
|
|
||||||
# Check free space in output
|
# Check free space in output
|
||||||
self._check_is_enough_free_space()
|
self._check_is_enough_free_space()
|
||||||
|
@ -2012,9 +2016,9 @@ def backup_create(name=None, description=None, repos=[],
|
||||||
if repos == []:
|
if repos == []:
|
||||||
repos = ['/home/yunohost.backup/archives']
|
repos = ['/home/yunohost.backup/archives']
|
||||||
|
|
||||||
methods = []
|
|
||||||
for repo in repos:
|
for repo in repos:
|
||||||
backup_manager.add(BackupMethod.create(methods, repo))
|
repo = BackupRepository.get(repo)
|
||||||
|
backup_manager.add(repo.method)
|
||||||
|
|
||||||
# Add backup targets (system and apps)
|
# Add backup targets (system and apps)
|
||||||
backup_manager.set_system_targets(system)
|
backup_manager.set_system_targets(system)
|
||||||
|
|
|
@ -77,7 +77,8 @@ class BackupRepository(object):
|
||||||
try:
|
try:
|
||||||
cls.repositories = read_json(REPOSITORIES_PATH)
|
cls.repositories = read_json(REPOSITORIES_PATH)
|
||||||
except MoulinetteError as e:
|
except MoulinetteError as e:
|
||||||
raise YunohostError('backup_cant_open_repositories_file', reason=e)
|
raise YunohostError(
|
||||||
|
'backup_cant_open_repositories_file', reason=e)
|
||||||
return cls.repositories
|
return cls.repositories
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -98,7 +99,8 @@ class BackupRepository(object):
|
||||||
|
|
||||||
self.name = location if name is None else name
|
self.name = location if name is None else name
|
||||||
if created and self.name in BackupMethod.repositories:
|
if created and self.name in BackupMethod.repositories:
|
||||||
raise YunohostError('backup_repository_already_exists', repositories=self.name)
|
raise YunohostError(
|
||||||
|
'backup_repository_already_exists', repositories=self.name)
|
||||||
|
|
||||||
self.description = description
|
self.description = description
|
||||||
self.encryption = encryption
|
self.encryption = encryption
|
||||||
|
@ -106,11 +108,7 @@ class BackupRepository(object):
|
||||||
|
|
||||||
if method is None:
|
if method is None:
|
||||||
method = 'tar' if self.domain is None else 'borg'
|
method = 'tar' if self.domain is None else 'borg'
|
||||||
if created:
|
self.method = BackupMethod.get(method, self)
|
||||||
self.method = BackupMethod.create(method, self)
|
|
||||||
else:
|
|
||||||
self.method = BackupMethod.get(method, self)
|
|
||||||
|
|
||||||
|
|
||||||
def compute_space_used(self):
|
def compute_space_used(self):
|
||||||
if self.used is None:
|
if self.used is None:
|
||||||
|
@ -128,127 +126,127 @@ class BackupRepository(object):
|
||||||
|
|
||||||
repositories.pop(self.name)
|
repositories.pop(self.name)
|
||||||
|
|
||||||
BackupRepository.save()
|
BackupRepository.save()
|
||||||
|
|
||||||
if purge:
|
if purge:
|
||||||
self.purge()
|
self.purge()
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
BackupRepository.reposirories[self.name] = self.__dict__
|
BackupRepository.reposirories[self.name] = self.__dict__
|
||||||
BackupRepository.save()
|
BackupRepository.save()
|
||||||
|
|
||||||
def _split_location(self):
|
def _split_location(self):
|
||||||
|
"""
|
||||||
|
Split a repository location into protocol, user, domain and path
|
||||||
|
"""
|
||||||
|
location_regex = r'^((?P<protocol>ssh://)?(?P<user>[^@ ]+)@(?P<domain>[^: ]+:))?(?P<path>[^\0]+)$'
|
||||||
|
location_match = re.match(location_regex, self.location)
|
||||||
|
|
||||||
|
if location_match is None:
|
||||||
|
raise YunohostError('backup_repositories_invalid_location',
|
||||||
|
location=location)
|
||||||
|
|
||||||
|
self.protocol = location_match.group('protocol')
|
||||||
|
self.user = location_match.group('user')
|
||||||
|
self.domain = location_match.group('domain')
|
||||||
|
self.path = location_match.group('path')
|
||||||
|
|
||||||
|
|
||||||
|
def backup_repository_list(name, full=False):
|
||||||
"""
|
"""
|
||||||
Split a repository location into protocol, user, domain and path
|
List available repositories where put archives
|
||||||
"""
|
"""
|
||||||
location_regex = r'^((?P<protocol>ssh://)?(?P<user>[^@ ]+)@(?P<domain>[^: ]+:))?(?P<path>[^\0]+)$'
|
repositories = BackupRepository.load()
|
||||||
location_match = re.match(location_regex, self.location)
|
|
||||||
|
|
||||||
if location_match is None:
|
if full:
|
||||||
raise YunohostError('backup_repositories_invalid_location',
|
return repositories
|
||||||
location=location)
|
else:
|
||||||
|
return repositories.keys()
|
||||||
self.protocol = location_match.group('protocol')
|
|
||||||
self.user = location_match.group('user')
|
|
||||||
self.domain = location_match.group('domain')
|
|
||||||
self.path = location_match.group('path')
|
|
||||||
|
|
||||||
|
|
||||||
def backup_repository_list(name, full=False):
|
def backup_repository_info(name, human_readable=True, space_used=False):
|
||||||
"""
|
"""
|
||||||
List available repositories where put archives
|
Show info about a repository
|
||||||
"""
|
|
||||||
repositories = BackupRepository.load()
|
|
||||||
|
|
||||||
if full:
|
Keyword arguments:
|
||||||
return repositories
|
name -- Name of the backup repository
|
||||||
else:
|
"""
|
||||||
return repositories.keys()
|
repository = BackupRepository.get(name)
|
||||||
|
|
||||||
|
if space_used:
|
||||||
|
repository.compute_space_used()
|
||||||
|
|
||||||
|
repository = repository.__dict__
|
||||||
|
if human_readable:
|
||||||
|
if 'quota' in repository:
|
||||||
|
repository['quota'] = binary_to_human(repository['quota'])
|
||||||
|
if 'used' in repository and isinstance(repository['used'], int):
|
||||||
|
repository['used'] = binary_to_human(repository['used'])
|
||||||
|
|
||||||
|
return repository
|
||||||
|
|
||||||
|
|
||||||
def backup_repository_info(name, human_readable=True, space_used=False):
|
@is_unit_operation()
|
||||||
"""
|
def backup_repository_add(operation_logger, location, name, description=None,
|
||||||
Show info about a repository
|
methods=None, quota=None, encryption="passphrase"):
|
||||||
|
"""
|
||||||
|
Add a backup repository
|
||||||
|
|
||||||
Keyword arguments:
|
Keyword arguments:
|
||||||
name -- Name of the backup repository
|
location -- Location of the repository (could be a remote location)
|
||||||
"""
|
name -- Name of the backup repository
|
||||||
repository = BackupRepository.get(name)
|
description -- An optionnal description
|
||||||
|
quota -- Maximum size quota of the repository
|
||||||
|
encryption -- If available, the kind of encryption to use
|
||||||
|
"""
|
||||||
|
repository = BackupRepository(
|
||||||
|
location, name, description, methods, quota, encryption)
|
||||||
|
|
||||||
if space_used:
|
try:
|
||||||
repository.compute_space_used()
|
repository.save()
|
||||||
|
except MoulinetteError:
|
||||||
|
raise YunohostError('backup_repository_add_failed',
|
||||||
|
repository=name, location=location)
|
||||||
|
|
||||||
repository = repository.__dict__
|
logger.success(m18n.n('backup_repository_added',
|
||||||
if human_readable:
|
repository=name, location=location))
|
||||||
if 'quota' in repository:
|
|
||||||
repository['quota'] = binary_to_human(repository['quota'])
|
|
||||||
if 'used' in repository and isinstance(repository['used'], int):
|
|
||||||
repository['used'] = binary_to_human(repository['used'])
|
|
||||||
|
|
||||||
return repository
|
|
||||||
|
|
||||||
|
|
||||||
@is_unit_operation()
|
@is_unit_operation()
|
||||||
def backup_repository_add(operation_logger, location, name, description=None,
|
def backup_repository_update(operation_logger, name, description=None,
|
||||||
methods=None, quota=None, encryption="passphrase"):
|
quota=None, password=None):
|
||||||
"""
|
"""
|
||||||
Add a backup repository
|
Update a backup repository
|
||||||
|
|
||||||
Keyword arguments:
|
Keyword arguments:
|
||||||
location -- Location of the repository (could be a remote location)
|
name -- Name of the backup repository
|
||||||
name -- Name of the backup repository
|
"""
|
||||||
description -- An optionnal description
|
repository = BackupRepository.get(name)
|
||||||
quota -- Maximum size quota of the repository
|
|
||||||
encryption -- If available, the kind of encryption to use
|
|
||||||
"""
|
|
||||||
repository = BackupRepository(
|
|
||||||
location, name, description, methods, quota, encryption)
|
|
||||||
|
|
||||||
try:
|
if description is not None:
|
||||||
repository.save()
|
repository.description = description
|
||||||
except MoulinetteError:
|
|
||||||
raise YunohostError('backup_repository_add_failed',
|
|
||||||
repository=name, location=location)
|
|
||||||
|
|
||||||
logger.success(m18n.n('backup_repository_added',
|
if quota is not None:
|
||||||
repository=name, location=location))
|
repository.quota = quota
|
||||||
|
|
||||||
|
try:
|
||||||
|
repository.save()
|
||||||
|
except MoulinetteError:
|
||||||
|
raise YunohostError('backup_repository_update_failed', repository=name)
|
||||||
|
logger.success(m18n.n('backup_repository_updated', repository=name,
|
||||||
|
location=repository['location']))
|
||||||
|
|
||||||
|
|
||||||
@is_unit_operation()
|
@is_unit_operation()
|
||||||
def backup_repository_update(operation_logger, name, description=None,
|
def backup_repository_remove(operation_logger, name, purge=False):
|
||||||
quota=None, password=None):
|
"""
|
||||||
"""
|
Remove a backup repository
|
||||||
Update a backup repository
|
|
||||||
|
|
||||||
Keyword arguments:
|
Keyword arguments:
|
||||||
name -- Name of the backup repository
|
name -- Name of the backup repository to remove
|
||||||
"""
|
|
||||||
repository = BackupRepository.get(name)
|
|
||||||
|
|
||||||
if description is not None:
|
"""
|
||||||
repository.description = description
|
repository = BackupRepository.get(name)
|
||||||
|
repository.delete(purge)
|
||||||
if quota is not None:
|
logger.success(m18n.n('backup_repository_removed', repository=name,
|
||||||
repository.quota = quota
|
path=repository['path']))
|
||||||
|
|
||||||
try:
|
|
||||||
repository.save()
|
|
||||||
except MoulinetteError:
|
|
||||||
raise YunohostError('backup_repository_update_failed', repository=name)
|
|
||||||
logger.success(m18n.n('backup_repository_updated', repository=name,
|
|
||||||
location=repository['location']))
|
|
||||||
|
|
||||||
|
|
||||||
@is_unit_operation()
|
|
||||||
def backup_repository_remove(operation_logger, name, purge=False):
|
|
||||||
"""
|
|
||||||
Remove a backup repository
|
|
||||||
|
|
||||||
Keyword arguments:
|
|
||||||
name -- Name of the backup repository to remove
|
|
||||||
|
|
||||||
"""
|
|
||||||
repository = BackupRepository.get(name)
|
|
||||||
repository.delete(purge)
|
|
||||||
logger.success(m18n.n('backup_repository_removed', repository=name,
|
|
||||||
path=repository['path']))
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue