From 752a0b807e7cc0133f4818f6c11ff4df7761a855 Mon Sep 17 00:00:00 2001 From: Houkime <> Date: Mon, 7 Aug 2023 13:33:10 +0000 Subject: [PATCH] feature(backups): lock and unlock at will --- .../backup/backuppers/restic_backupper.py | 53 ++++++++++++++++++- selfprivacy_api/backup/util.py | 16 ++++-- tests/test_graphql/test_backup.py | 15 ++++++ 3 files changed, 78 insertions(+), 6 deletions(-) diff --git a/selfprivacy_api/backup/backuppers/restic_backupper.py b/selfprivacy_api/backup/backuppers/restic_backupper.py index 37ae06b..a359f98 100644 --- a/selfprivacy_api/backup/backuppers/restic_backupper.py +++ b/selfprivacy_api/backup/backuppers/restic_backupper.py @@ -227,6 +227,24 @@ class ResticBackupper(AbstractBackupper): raise ValueError("cannot init a repo: " + output) def is_initted(self) -> bool: + command = self.restic_command( + "check", + ) + + with subprocess.Popen( + command, + stdout=subprocess.PIPE, + shell=False, + stderr=subprocess.STDOUT, + ) as handle: + # communication forces to complete and for returncode to get defined + output = handle.communicate()[0].decode("utf-8") + if handle.returncode != 0: + return False + return True + + def unlock(self) -> None: + """Remove stale locks.""" command = self.restic_command( "unlock", ) @@ -235,10 +253,41 @@ class ResticBackupper(AbstractBackupper): command, stdout=subprocess.PIPE, shell=False, + stderr=subprocess.STDOUT, ) as handle: + # communication forces to complete and for returncode to get defined + output = handle.communicate()[0].decode("utf-8") if handle.returncode != 0: - return False - return True + raise ValueError("cannot unlock the backup repository: ", output) + + def lock(self) -> None: + """ + Introduce a stale lock. + Mainly for testing purposes. + Double lock is supposed to fail + """ + command = self.restic_command( + "check", + ) + + # using temporary cache in /run/user/1000/restic-check-cache-817079729 + # repository 9639c714 opened (repository version 2) successfully, password is correct + # created new cache in /run/user/1000/restic-check-cache-817079729 + # create exclusive lock for repository + # load indexes + # check all packs + # check snapshots, trees and blobs + # [0:00] 100.00% 1 / 1 snapshots + # no errors were found + + try: + for line in output_yielder(command): + if "indexes" in line: + break + if "unable" in line: + raise ValueError(line) + except Exception as e: + raise ValueError("could not lock repository") from e def restored_size(self, snapshot_id: str) -> int: """ diff --git a/selfprivacy_api/backup/util.py b/selfprivacy_api/backup/util.py index bda421e..41d926c 100644 --- a/selfprivacy_api/backup/util.py +++ b/selfprivacy_api/backup/util.py @@ -1,8 +1,10 @@ import subprocess from os.path import exists +from typing import Generator -def output_yielder(command): +def output_yielder(command) -> Generator[str, None, None]: + """Note: If you break during iteration, it kills the process""" with subprocess.Popen( command, shell=False, @@ -10,9 +12,15 @@ def output_yielder(command): stderr=subprocess.STDOUT, universal_newlines=True, ) as handle: - for line in iter(handle.stdout.readline, ""): - if "NOTICE:" not in line: - yield line + if handle is None or handle.stdout is None: + raise ValueError("could not run command: ", command) + + try: + for line in iter(handle.stdout.readline, ""): + if "NOTICE:" not in line: + yield line + except GeneratorExit: + handle.kill() def sync(src_path: str, dest_path: str): diff --git a/tests/test_graphql/test_backup.py b/tests/test_graphql/test_backup.py index da81c60..9743567 100644 --- a/tests/test_graphql/test_backup.py +++ b/tests/test_graphql/test_backup.py @@ -758,3 +758,18 @@ def test_move_blocks_backups(backups, dummy_service, restore_strategy): with pytest.raises(ValueError): Backups.restore_snapshot(snap, restore_strategy) + + +def test_double_lock_unlock(backups, dummy_service): + # notice that introducing stale locks is only safe for other tests if we erase repo in between + # which we do at the time of writing this test + + Backups.provider().backupper.lock() + with pytest.raises(ValueError): + Backups.provider().backupper.lock() + + Backups.provider().backupper.unlock() + Backups.provider().backupper.lock() + + Backups.provider().backupper.unlock() + Backups.provider().backupper.unlock()