2023-02-03 20:28:55 +00:00
|
|
|
import subprocess
|
2023-02-08 16:28:05 +00:00
|
|
|
import json
|
2023-02-03 20:28:55 +00:00
|
|
|
|
2023-02-13 11:16:35 +00:00
|
|
|
from typing import List
|
|
|
|
|
2023-02-08 14:57:34 +00:00
|
|
|
from selfprivacy_api.backup.backuper import AbstractBackuper
|
2023-02-13 11:16:35 +00:00
|
|
|
from selfprivacy_api.models.backup.snapshot import Snapshot
|
2023-01-23 13:43:18 +00:00
|
|
|
|
2023-02-17 15:55:19 +00:00
|
|
|
from selfprivacy_api.backup.local_secret import LocalBackupSecret
|
|
|
|
|
2023-01-23 13:43:18 +00:00
|
|
|
|
|
|
|
class ResticBackuper(AbstractBackuper):
|
2023-01-23 14:21:43 +00:00
|
|
|
def __init__(self, login_flag: str, key_flag: str, type: str):
|
|
|
|
self.login_flag = login_flag
|
|
|
|
self.key_flag = key_flag
|
|
|
|
self.type = type
|
2023-02-03 20:28:55 +00:00
|
|
|
self.account = ""
|
|
|
|
self.key = ""
|
|
|
|
|
|
|
|
def set_creds(self, account: str, key: str):
|
|
|
|
self.account = account
|
|
|
|
self.key = key
|
2023-01-23 14:21:43 +00:00
|
|
|
|
|
|
|
def restic_repo(self, repository_name: str) -> str:
|
|
|
|
# https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#other-services-via-rclone
|
|
|
|
# https://forum.rclone.org/t/can-rclone-be-run-solely-with-command-line-options-no-config-no-env-vars/6314/5
|
2023-02-20 11:32:25 +00:00
|
|
|
return f"rclone:{self.type}{repository_name}/sfbackup"
|
2023-01-23 14:21:43 +00:00
|
|
|
|
|
|
|
def rclone_args(self):
|
|
|
|
return "rclone.args=serve restic --stdio" + self.backend_rclone_args()
|
|
|
|
|
2023-02-08 14:05:25 +00:00
|
|
|
def backend_rclone_args(self) -> str:
|
2023-02-03 18:03:13 +00:00
|
|
|
acc_arg = ""
|
|
|
|
key_arg = ""
|
2023-02-08 14:05:25 +00:00
|
|
|
if self.account != "":
|
|
|
|
acc_arg = f"{self.login_flag} {self.account}"
|
|
|
|
if self.key != "":
|
|
|
|
key_arg = f"{self.key_flag} {self.key}"
|
2023-02-03 18:03:13 +00:00
|
|
|
|
|
|
|
return f"{acc_arg} {key_arg}"
|
2023-01-23 14:21:43 +00:00
|
|
|
|
2023-02-17 15:55:19 +00:00
|
|
|
def _password_command(self):
|
|
|
|
return f"echo {LocalBackupSecret.get()}"
|
|
|
|
|
2023-02-08 14:05:25 +00:00
|
|
|
def restic_command(self, repo_name: str, *args):
|
|
|
|
command = [
|
2023-01-23 14:21:43 +00:00
|
|
|
"restic",
|
|
|
|
"-o",
|
|
|
|
self.rclone_args(),
|
|
|
|
"-r",
|
2023-02-08 14:05:25 +00:00
|
|
|
self.restic_repo(repo_name),
|
2023-02-17 15:55:19 +00:00
|
|
|
"--password-command",
|
|
|
|
self._password_command(),
|
2023-02-08 14:05:25 +00:00
|
|
|
]
|
|
|
|
if args != []:
|
|
|
|
command.extend(args)
|
|
|
|
return command
|
2023-02-03 20:28:55 +00:00
|
|
|
|
2023-02-08 14:05:25 +00:00
|
|
|
def start_backup(self, folder: str, repo_name: str):
|
2023-02-03 20:28:55 +00:00
|
|
|
"""
|
|
|
|
Start backup with restic
|
|
|
|
"""
|
|
|
|
backup_command = self.restic_command(
|
2023-02-08 14:05:25 +00:00
|
|
|
repo_name,
|
2023-02-03 20:28:55 +00:00
|
|
|
"backup",
|
|
|
|
folder,
|
|
|
|
)
|
2023-02-22 10:07:05 +00:00
|
|
|
with subprocess.Popen(
|
2023-02-08 14:05:25 +00:00
|
|
|
backup_command,
|
|
|
|
shell=False,
|
2023-02-22 10:07:05 +00:00
|
|
|
stdout=subprocess.PIPE,
|
2023-02-08 14:05:25 +00:00
|
|
|
stderr=subprocess.STDOUT,
|
2023-02-22 10:07:05 +00:00
|
|
|
) as handle:
|
|
|
|
output = handle.communicate()[0].decode("utf-8")
|
|
|
|
if "saved" not in output:
|
|
|
|
raise ValueError("could not create a new snapshot: " + output)
|
2023-02-08 15:40:45 +00:00
|
|
|
|
2023-02-17 15:59:27 +00:00
|
|
|
def init(self, repo_name):
|
|
|
|
init_command = self.restic_command(
|
|
|
|
repo_name,
|
|
|
|
"init",
|
|
|
|
)
|
2023-02-20 13:04:39 +00:00
|
|
|
with subprocess.Popen(
|
2023-02-17 15:59:27 +00:00
|
|
|
init_command,
|
|
|
|
shell=False,
|
2023-02-20 13:04:39 +00:00
|
|
|
stdout=subprocess.PIPE,
|
2023-02-17 15:59:27 +00:00
|
|
|
stderr=subprocess.STDOUT,
|
2023-02-20 13:04:39 +00:00
|
|
|
) as process_handle:
|
|
|
|
output = process_handle.communicate()[0].decode("utf-8")
|
|
|
|
if not "created restic repository" in output:
|
|
|
|
raise ValueError("cannot init a repo: " + output)
|
2023-02-17 15:59:27 +00:00
|
|
|
|
2023-02-08 15:40:45 +00:00
|
|
|
def restore_from_backup(self, repo_name, snapshot_id, folder):
|
|
|
|
"""
|
|
|
|
Restore from backup with restic
|
|
|
|
"""
|
|
|
|
restore_command = self.restic_command(
|
|
|
|
repo_name, "restore", snapshot_id, "--target", folder
|
|
|
|
)
|
|
|
|
|
2023-02-22 14:45:11 +00:00
|
|
|
with subprocess.Popen(
|
|
|
|
restore_command, stdout=subprocess.PIPE, shell=False
|
|
|
|
) as handle:
|
|
|
|
|
|
|
|
output = handle.communicate()[0].decode("utf-8")
|
|
|
|
if "restored" not in output:
|
|
|
|
raise ValueError("cannot restore a snapshot: " + output)
|
2023-02-08 16:28:05 +00:00
|
|
|
|
|
|
|
def _load_snapshots(self, repo_name) -> object:
|
|
|
|
"""
|
|
|
|
Load list of snapshots from repository
|
2023-02-17 15:55:19 +00:00
|
|
|
raises Value Error if repo does not exist
|
2023-02-08 16:28:05 +00:00
|
|
|
"""
|
|
|
|
listing_command = self.restic_command(
|
|
|
|
repo_name,
|
|
|
|
"snapshots",
|
|
|
|
"--json",
|
|
|
|
)
|
|
|
|
|
|
|
|
with subprocess.Popen(
|
|
|
|
listing_command,
|
|
|
|
shell=False,
|
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
stderr=subprocess.STDOUT,
|
|
|
|
) as backup_listing_process_descriptor:
|
|
|
|
output = backup_listing_process_descriptor.communicate()[0].decode("utf-8")
|
|
|
|
|
2023-02-17 15:55:19 +00:00
|
|
|
if "Is there a repository at the following location?" in output:
|
|
|
|
raise ValueError("No repository! : " + output)
|
2023-02-08 16:28:05 +00:00
|
|
|
try:
|
|
|
|
return self.parse_snapshot_output(output)
|
2023-02-17 15:55:19 +00:00
|
|
|
except ValueError as e:
|
|
|
|
raise ValueError("Cannot load snapshots: ") from e
|
2023-02-08 16:28:05 +00:00
|
|
|
|
2023-02-13 11:16:35 +00:00
|
|
|
def get_snapshots(self, repo_name) -> List[Snapshot]:
|
|
|
|
"""Get all snapshots from the repo"""
|
2023-02-08 16:28:05 +00:00
|
|
|
snapshots = []
|
2023-02-22 13:35:55 +00:00
|
|
|
for restic_snapshot in self._load_snapshots(repo_name):
|
|
|
|
snapshot = Snapshot(
|
|
|
|
id=restic_snapshot["short_id"],
|
|
|
|
created_at=restic_snapshot["time"],
|
|
|
|
service_name=repo_name,
|
|
|
|
)
|
|
|
|
|
2023-02-08 16:28:05 +00:00
|
|
|
snapshots.append(snapshot)
|
|
|
|
return snapshots
|
|
|
|
|
|
|
|
def parse_snapshot_output(self, output: str) -> object:
|
2023-02-17 15:55:19 +00:00
|
|
|
if "[" not in output:
|
2023-02-22 13:46:28 +00:00
|
|
|
raise ValueError(
|
|
|
|
"There is no json in the restic snapshot output : " + output
|
|
|
|
)
|
2023-02-08 16:28:05 +00:00
|
|
|
starting_index = output.find("[")
|
2023-02-17 15:55:19 +00:00
|
|
|
return json.loads(output[starting_index:])
|