diff --git a/selfprivacy_api/backup/local_secret.py b/selfprivacy_api/backup/local_secret.py index 76237f3..ea2afec 100644 --- a/selfprivacy_api/backup/local_secret.py +++ b/selfprivacy_api/backup/local_secret.py @@ -15,13 +15,13 @@ redis = RedisPool().get_connection() class LocalBackupSecret: @staticmethod - def get(): + def get() -> str: """A secret string which backblaze/other clouds do not know. Serves as encryption key. """ if not LocalBackupSecret.exists(): LocalBackupSecret.reset() - return redis.get(REDIS_KEY) + return redis.get(REDIS_KEY) # type: ignore @staticmethod def set(secret: str): @@ -38,7 +38,7 @@ class LocalBackupSecret: @staticmethod def exists() -> bool: - return redis.exists(REDIS_KEY) + return redis.exists(REDIS_KEY) == 1 @staticmethod def _generate() -> str: diff --git a/selfprivacy_api/backup/providers/backblaze.py b/selfprivacy_api/backup/providers/backblaze.py index 9ec5eba..f474a99 100644 --- a/selfprivacy_api/backup/providers/backblaze.py +++ b/selfprivacy_api/backup/providers/backblaze.py @@ -4,3 +4,5 @@ from selfprivacy_api.backup.restic_backuper import ResticBackuper class Backblaze(AbstractBackupProvider): backuper = ResticBackuper("--b2-account", "--b2-key", ":b2:") + + name = "BACKBLAZE" diff --git a/selfprivacy_api/backup/providers/local_file.py b/selfprivacy_api/backup/providers/local_file.py index a20f615..95075dd 100644 --- a/selfprivacy_api/backup/providers/local_file.py +++ b/selfprivacy_api/backup/providers/local_file.py @@ -5,6 +5,8 @@ from selfprivacy_api.backup.restic_backuper import ResticBackuper class LocalFileBackup(AbstractBackupProvider): backuper = ResticBackuper("", "", "memory") + name = "FILE" + # login and key args are for compatibility with generic provider methods. They are ignored. def __init__(self, filename: str, login: str = "", key: str = ""): super().__init__() diff --git a/selfprivacy_api/backup/providers/memory.py b/selfprivacy_api/backup/providers/memory.py index 3f257bf..a481559 100644 --- a/selfprivacy_api/backup/providers/memory.py +++ b/selfprivacy_api/backup/providers/memory.py @@ -4,3 +4,5 @@ from selfprivacy_api.backup.restic_backuper import ResticBackuper class InMemoryBackup(AbstractBackupProvider): backuper = ResticBackuper("", "", ":memory:") + + name = "MEMORY" diff --git a/selfprivacy_api/backup/providers/provider.py b/selfprivacy_api/backup/providers/provider.py index c303d4e..fcf179b 100644 --- a/selfprivacy_api/backup/providers/provider.py +++ b/selfprivacy_api/backup/providers/provider.py @@ -12,6 +12,8 @@ class AbstractBackupProvider(ABC): def backuper(self) -> AbstractBackuper: raise NotImplementedError + name = "NONE" + def __init__(self, login="", key="", location="", repo_id=""): self.backuper.set_creds(login, key, location) self.login = login diff --git a/selfprivacy_api/graphql/common_types/backup_snapshot.py b/selfprivacy_api/graphql/common_types/backup_snapshot.py deleted file mode 100644 index 3256e0c..0000000 --- a/selfprivacy_api/graphql/common_types/backup_snapshot.py +++ /dev/null @@ -1,9 +0,0 @@ -import datetime -import strawberry - - -@strawberry.type -class SnapshotInfo: - id: str - service_name: str - created_at: datetime.datetime diff --git a/selfprivacy_api/graphql/common_types/service.py b/selfprivacy_api/graphql/common_types/service.py index 9e04254..b3403e9 100644 --- a/selfprivacy_api/graphql/common_types/service.py +++ b/selfprivacy_api/graphql/common_types/service.py @@ -3,7 +3,6 @@ import typing import strawberry import datetime from selfprivacy_api.graphql.common_types.dns import DnsRecord -from selfprivacy_api.graphql.common_types.backup_snapshot import SnapshotInfo from selfprivacy_api.services import get_service_by_id, get_services_by_location from selfprivacy_api.services import Service as ServiceInterface @@ -104,14 +103,14 @@ class Service: return get_storage_usage(self) @strawberry.field - def backup_snapshots(self) -> typing.Optional[typing.List[SnapshotInfo]]: + def backup_snapshots(self) -> typing.Optional[typing.List["SnapshotInfo"]]: return None @strawberry.type class SnapshotInfo: id: str - service: "Service" + service: Service created_at: datetime.datetime diff --git a/selfprivacy_api/graphql/queries/backup.py b/selfprivacy_api/graphql/queries/backup.py index 97b4682..9858543 100644 --- a/selfprivacy_api/graphql/queries/backup.py +++ b/selfprivacy_api/graphql/queries/backup.py @@ -7,41 +7,70 @@ import strawberry from selfprivacy_api.backup import Backups from selfprivacy_api.backup.local_secret import LocalBackupSecret from selfprivacy_api.graphql.queries.providers import BackupProvider -from selfprivacy_api.graphql.common_types.service import SnapshotInfo +from selfprivacy_api.graphql.common_types.service import ( + Service, + ServiceStatusEnum, + SnapshotInfo, + service_to_graphql_service, +) +from selfprivacy_api.services import get_service_by_id @strawberry.type class BackupConfiguration: provider: BackupProvider - # When server is lost, the app should have the key to decrypt backups on a new server + # When server is lost, the app should have the key to decrypt backups + # on a new server encryption_key: str # False when repo is not initialized and not ready to be used is_initialized: bool # If none, autobackups are disabled - autobackup_period: typing.Optional[int] = None + autobackup_period: typing.Optional[int] # Bucket name for Backblaze, path for some other providers - location_name: typing.Optional[str] = None - location_id: typing.Optional[str] = None + location_name: typing.Optional[str] + location_id: typing.Optional[str] @strawberry.type class Backup: @strawberry.field - def configuration() -> BackupConfiguration: - config = BackupConfiguration() - config.encryption_key = LocalBackupSecret.get() - config.is_initialized = Backups.is_initted() - config.autobackup_period = Backups.autobackup_period_minutes() - config.location_name = Backups.provider().location - config.location_id = Backups.provider().repo_id + def configuration(self) -> BackupConfiguration: + return BackupConfiguration( + provider=BackupProvider[Backups.provider().name], + encryption_key=LocalBackupSecret.get(), + is_initialized=Backups.is_initted(), + autobackup_period=Backups.autobackup_period_minutes(), + location_name=Backups.provider().location, + location_id=Backups.provider().repo_id, + ) @strawberry.field def all_snapshots(self) -> typing.List[SnapshotInfo]: + if not Backups.is_initted(): + return [] result = [] snapshots = Backups.get_all_snapshots() for snap in snapshots: + service = get_service_by_id(snap.service_name) + if service is None: + service = Service( + id=snap.service_name, + display_name=f"{snap.service_name} (Orphaned)", + description="", + svg_icon="", + is_movable=False, + is_required=False, + is_enabled=False, + status=ServiceStatusEnum.OFF, + url=None, + dns_records=None, + ) + else: + service = service_to_graphql_service(service) graphql_snap = SnapshotInfo( - id=snap.id, service=snap.service_name, created_at=snap.created_at + id=snap.id, + service=service, + created_at=snap.created_at, ) result.append(graphql_snap) return result diff --git a/selfprivacy_api/graphql/queries/providers.py b/selfprivacy_api/graphql/queries/providers.py index 2a9fcec..b9ca7ef 100644 --- a/selfprivacy_api/graphql/queries/providers.py +++ b/selfprivacy_api/graphql/queries/providers.py @@ -19,6 +19,7 @@ class ServerProvider(Enum): @strawberry.enum class BackupProvider(Enum): BACKBLAZE = "BACKBLAZE" + NONE = "NONE" # for testing purposes, make sure not selectable in prod. MEMORY = "MEMORY" FILE = "FILE"