From d43a86b13662e1e749f1e21e7abf4e500e15d806 Mon Sep 17 00:00:00 2001 From: ljf Date: Wed, 2 Jan 2019 19:27:58 +0100 Subject: [PATCH] [wip] Remote borg repository --- data/actionsmap/yunohost.yml | 4 ++ src/yunohost/backup.py | 79 +++++++++++++++++++++++++----------- 2 files changed, 60 insertions(+), 23 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index a0b2447ce..9f1652335 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -891,6 +891,10 @@ backup: action_help: List available local backup archives api: GET /backup/archives arguments: + -r: + full: --repos + help: List archives in these repositories + nargs: "*" -i: full: --with-info help: Show backup information for each archive diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 62c3e32ce..054e6ac37 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -1889,42 +1889,75 @@ class TarBackupMethod(BackupMethod): class BorgBackupMethod(BackupMethod): def __init__(self, repo=None): - super(TarBackupMethod, self).__init__(repo) + super(BorgBackupMethod, self).__init__(repo) + if not self.repo.domain: filesystem.mkdir(self.repo.path, parent=True) - else: - #Todo Initialize remote repo - pass + + cmd = ['borg', 'init', self.repo.location] + + if self.repo.quota: + cmd += ['--storage-quota', self.repo.quota] + borg = self._run_borg_command(cmd) + return_code = borg.wait() + if return_code: + raise YunohostError('backup_borg_init_error') + @property def method_name(self): return 'borg' + + def need_mount(self): + return True def backup(self): """ Backup prepared files with borg """ - super(BorgBackupMethod, self).backup() + + archive = self.repo.location + '::' + self.name + cmd = ['borg', 'create', archive, './'] + borg = self._run_borg_command(cmd) + return_code = borg.wait() + if return_code: + raise YunohostError('backup_borg_mount_error') + + def mount(self, restore_manager): + """ Extract and mount needed files with borg """ + super(BorgBackupMethod, self).mount(restore_manager) - for path in self.manager.paths_to_backup: - source = path['source'] - dest = os.path.join(self.repo.path, path['dest']) - if source == dest: - logger.debug("Files already copyed") - return + # Export as tar needed files through a pipe + archive = self.repo.location + '::' + self.name + cmd = ['borg', 'export-tar', archive, '-'] + borg = self._run_borg_command(cmd, stdout=subprocess.PIPE) - dest_parent = os.path.dirname(dest) - if not os.path.exists(dest_parent): - filesystem.mkdir(dest_parent, 0o750, True, uid='admin') + # And uncompress it into the working directory + untar = subprocess.Popen(['tar', 'x'], cwd=self.work_dir, stdin=borg.stdout) + borg_return_code = borg.wait() + untar_return_code = untar.wait() + if borg_return_code + untar_return_code != 0: + err = untar.communicate()[1] + raise YunohostError('backup_borg_backup_error') - if os.path.isdir(source): - shutil.copytree(source, dest) - else: - shutil.copy(source, dest) + def _run_borg_command(self, cmd, stdout=None): + env = dict(os.environ) - # TODO run borg create command - raise YunohostError('backup_borg_not_implemented') + if self.repo.domain: + # TODO Use the best/good key + private_key = "/root/.ssh/ssh_host_ed25519_key" + + # Don't check ssh fingerprint strictly the first time + # TODO improve this by publishing and checking this with DNS + strict = 'yes' if self.repo.domain in open('/root/.ssh/known_hosts').read() else 'no' + env['BORG_RSH'] = "ssh -i %s -oStrictHostKeyChecking=%s" + env['BORG_RSH'] = env['BORG_RSH'] % (private_key, strict) + + # In case, borg need a passphrase to get access to the repo + if self.repo.passphrase: + cmd += ['-e', 'repokey'] + env['BORG_PASSPHRASE'] = self.repo.passphrase + + return subprocess.Popen(cmd, env=env, stdout=stdout) - def mount(self, mnt_path): - raise YunohostError('backup_borg_not_implemented') class CustomBackupMethod(BackupMethod): @@ -1943,7 +1976,7 @@ class CustomBackupMethod(BackupMethod): @property def method_name(self): - return 'borg' + return 'custom' def need_mount(self): """Call the backup_method hook to know if we need to organize files