mirror of
https://git.selfprivacy.org/SelfPrivacy/selfprivacy-rest-api.git
synced 2024-11-18 08:29:14 +00:00
feature(backups): add full repo erasure capability
This commit is contained in:
parent
ffec344ba8
commit
cfa7f4ae59
|
@ -236,6 +236,14 @@ class Backups:
|
||||||
Backups.provider().backupper.init()
|
Backups.provider().backupper.init()
|
||||||
Storage.mark_as_init()
|
Storage.mark_as_init()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def erase_repo() -> None:
|
||||||
|
"""
|
||||||
|
Completely empties the remote
|
||||||
|
"""
|
||||||
|
Backups.provider().backupper.erase_repo()
|
||||||
|
Storage.mark_as_uninitted()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_initted() -> bool:
|
def is_initted() -> bool:
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -36,6 +36,11 @@ class AbstractBackupper(ABC):
|
||||||
"""Initialize the repository"""
|
"""Initialize the repository"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def erase_repo(self) -> None:
|
||||||
|
"""Completely empties the remote"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def restore_from_backup(
|
def restore_from_backup(
|
||||||
self,
|
self,
|
||||||
|
|
|
@ -23,6 +23,10 @@ class NoneBackupper(AbstractBackupper):
|
||||||
def init(self):
|
def init(self):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def erase_repo(self) -> None:
|
||||||
|
"""Completely empties the remote"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
def restore_from_backup(self, snapshot_id: str, folders: List[str], verify=True):
|
def restore_from_backup(self, snapshot_id: str, folders: List[str], verify=True):
|
||||||
"""Restore a target folder using a snapshot"""
|
"""Restore a target folder using a snapshot"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
|
@ -40,20 +40,25 @@ class ResticBackupper(AbstractBackupper):
|
||||||
def restic_repo(self) -> str:
|
def restic_repo(self) -> str:
|
||||||
# https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#other-services-via-rclone
|
# 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
|
# https://forum.rclone.org/t/can-rclone-be-run-solely-with-command-line-options-no-config-no-env-vars/6314/5
|
||||||
return f"rclone:{self.storage_type}{self.repo}"
|
return f"rclone:{self.rclone_repo()}"
|
||||||
|
|
||||||
|
def rclone_repo(self) -> str:
|
||||||
|
return f"{self.storage_type}{self.repo}"
|
||||||
|
|
||||||
def rclone_args(self):
|
def rclone_args(self):
|
||||||
return "rclone.args=serve restic --stdio " + self.backend_rclone_args()
|
return "rclone.args=serve restic --stdio " + " ".join(
|
||||||
|
self.backend_rclone_args()
|
||||||
|
)
|
||||||
|
|
||||||
def backend_rclone_args(self) -> str:
|
def backend_rclone_args(self) -> list[str]:
|
||||||
acc_arg = ""
|
args = []
|
||||||
key_arg = ""
|
|
||||||
if self.account != "":
|
if self.account != "":
|
||||||
acc_arg = f"{self.login_flag} {self.account}"
|
acc_args = [self.login_flag, self.account]
|
||||||
|
args.extend(acc_args)
|
||||||
if self.key != "":
|
if self.key != "":
|
||||||
key_arg = f"{self.key_flag} {self.key}"
|
key_args = [self.key_flag, self.key]
|
||||||
|
args.extend(key_args)
|
||||||
return f"{acc_arg} {key_arg}"
|
return args
|
||||||
|
|
||||||
def _password_command(self):
|
def _password_command(self):
|
||||||
return f"echo {LocalBackupSecret.get()}"
|
return f"echo {LocalBackupSecret.get()}"
|
||||||
|
@ -79,6 +84,27 @@ class ResticBackupper(AbstractBackupper):
|
||||||
command.extend(ResticBackupper.__flatten_list(args))
|
command.extend(ResticBackupper.__flatten_list(args))
|
||||||
return command
|
return command
|
||||||
|
|
||||||
|
def erase_repo(self) -> None:
|
||||||
|
"""Fully erases repo on remote, can be reinitted again"""
|
||||||
|
command = [
|
||||||
|
"rclone",
|
||||||
|
"purge",
|
||||||
|
self.rclone_repo(),
|
||||||
|
]
|
||||||
|
backend_args = self.backend_rclone_args()
|
||||||
|
if backend_args:
|
||||||
|
command.extend(backend_args)
|
||||||
|
|
||||||
|
with subprocess.Popen(command, stdout=subprocess.PIPE, shell=False) as handle:
|
||||||
|
output = handle.communicate()[0].decode("utf-8")
|
||||||
|
if handle.returncode != 0:
|
||||||
|
raise ValueError(
|
||||||
|
"purge exited with errorcode",
|
||||||
|
handle.returncode,
|
||||||
|
":",
|
||||||
|
output,
|
||||||
|
)
|
||||||
|
|
||||||
def mount_repo(self, mount_directory):
|
def mount_repo(self, mount_directory):
|
||||||
mount_command = self.restic_command("mount", mount_directory)
|
mount_command = self.restic_command("mount", mount_directory)
|
||||||
mount_command.insert(0, "nohup")
|
mount_command.insert(0, "nohup")
|
||||||
|
|
|
@ -21,7 +21,7 @@ REDIS_SNAPSHOT_CACHE_EXPIRE_SECONDS = 24 * 60 * 60 # one day
|
||||||
|
|
||||||
REDIS_SNAPSHOTS_PREFIX = "backups:snapshots:"
|
REDIS_SNAPSHOTS_PREFIX = "backups:snapshots:"
|
||||||
REDIS_LAST_BACKUP_PREFIX = "backups:last-backed-up:"
|
REDIS_LAST_BACKUP_PREFIX = "backups:last-backed-up:"
|
||||||
REDIS_INITTED_CACHE_PREFIX = "backups:initted_services:"
|
REDIS_INITTED_CACHE = "backups:repo_initted"
|
||||||
|
|
||||||
REDIS_PROVIDER_KEY = "backups:provider"
|
REDIS_PROVIDER_KEY = "backups:provider"
|
||||||
REDIS_AUTOBACKUP_PERIOD_KEY = "backups:autobackup_period"
|
REDIS_AUTOBACKUP_PERIOD_KEY = "backups:autobackup_period"
|
||||||
|
@ -38,9 +38,9 @@ class Storage:
|
||||||
"""Deletes all backup related data from redis"""
|
"""Deletes all backup related data from redis"""
|
||||||
redis.delete(REDIS_PROVIDER_KEY)
|
redis.delete(REDIS_PROVIDER_KEY)
|
||||||
redis.delete(REDIS_AUTOBACKUP_PERIOD_KEY)
|
redis.delete(REDIS_AUTOBACKUP_PERIOD_KEY)
|
||||||
|
redis.delete(REDIS_INITTED_CACHE)
|
||||||
|
|
||||||
prefixes_to_clean = [
|
prefixes_to_clean = [
|
||||||
REDIS_INITTED_CACHE_PREFIX,
|
|
||||||
REDIS_SNAPSHOTS_PREFIX,
|
REDIS_SNAPSHOTS_PREFIX,
|
||||||
REDIS_LAST_BACKUP_PREFIX,
|
REDIS_LAST_BACKUP_PREFIX,
|
||||||
]
|
]
|
||||||
|
@ -162,11 +162,16 @@ class Storage:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def has_init_mark() -> bool:
|
def has_init_mark() -> bool:
|
||||||
"""Returns True if the repository was initialized"""
|
"""Returns True if the repository was initialized"""
|
||||||
if redis.exists(REDIS_INITTED_CACHE_PREFIX):
|
if redis.exists(REDIS_INITTED_CACHE):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def mark_as_init():
|
def mark_as_init():
|
||||||
"""Marks the repository as initialized"""
|
"""Marks the repository as initialized"""
|
||||||
redis.set(REDIS_INITTED_CACHE_PREFIX, 1)
|
redis.set(REDIS_INITTED_CACHE, 1)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def mark_as_uninitted():
|
||||||
|
"""Marks the repository as initialized"""
|
||||||
|
redis.delete(REDIS_INITTED_CACHE)
|
||||||
|
|
|
@ -222,6 +222,19 @@ def test_file_backend_init(file_backup):
|
||||||
file_backup.backupper.init()
|
file_backup.backupper.init()
|
||||||
|
|
||||||
|
|
||||||
|
def test_reinit_after_purge(backups):
|
||||||
|
assert Backups.is_initted() is True
|
||||||
|
|
||||||
|
Backups.erase_repo()
|
||||||
|
assert Backups.is_initted() is False
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
Backups.get_all_snapshots()
|
||||||
|
|
||||||
|
Backups.init_repo()
|
||||||
|
assert Backups.is_initted() is True
|
||||||
|
assert len(Backups.get_all_snapshots()) == 0
|
||||||
|
|
||||||
|
|
||||||
def test_backup_simple_file(raw_dummy_service, file_backup):
|
def test_backup_simple_file(raw_dummy_service, file_backup):
|
||||||
# temporarily incomplete
|
# temporarily incomplete
|
||||||
service = raw_dummy_service
|
service = raw_dummy_service
|
||||||
|
|
Loading…
Reference in a new issue