import datetime import json from json import JSONDecodeError from playhouse.shortcuts import dict_to_model, model_to_dict from vorta.keyring.abc import VortaKeyring from vorta.store.connection import DB, SCHEMA_VERSION, init_db from vorta.store.models import ( BackupProfileModel, RepoModel, SchemaVersion, SettingsModel, SourceFileModel, WifiSettingModel, ) class ProfileExport: def __init__(self, profile_dict): self._profile_dict = profile_dict @property def id(self): return self._profile_dict['id'] @property def name(self): return self._profile_dict['name'] @property def schema_version(self): return self._profile_dict['SchemaVersion']['version'] @property def repo_url(self): if ( 'repo' in self._profile_dict and isinstance(self._profile_dict['repo'], dict) and 'url' in self._profile_dict['repo'] ): return self._profile_dict['repo']['url'] return None @property def repo_password(self): return self._profile_dict['password'] if 'password' in self._profile_dict else None @repo_password.setter def repo_password(self, password): self._profile_dict['password'] = password @classmethod def from_db(cls, profile, store_password=True, include_settings=True): profile_dict = model_to_dict(profile, exclude=[RepoModel.id]) # Have to retain profile ID keyring = VortaKeyring.get_keyring() if store_password: profile_dict['password'] = keyring.get_password('vorta-repo', profile.repo.url) # For all below, exclude ids to prevent collisions. DB will automatically reassign ids # Add SourceFileModel profile_dict['SourceFileModel'] = [ model_to_dict(source, recurse=False, exclude=[SourceFileModel.id]) for source in SourceFileModel.select().where(SourceFileModel.profile == profile) ] # Add SchemaVersion profile_dict['SchemaVersion'] = model_to_dict(SchemaVersion.get(id=1)) if include_settings: # Add WifiSettingModel profile_dict['WifiSettingModel'] = [ model_to_dict(wifi, recurse=False) for wifi in WifiSettingModel.select().where(WifiSettingModel.profile == profile.id) ] # Add SettingsModel profile_dict['SettingsModel'] = [model_to_dict(s, exclude=[SettingsModel.id]) for s in SettingsModel] return ProfileExport(profile_dict) def to_db(self, overwrite_profile=False, overwrite_settings=True): profile_schema = self._profile_dict['SchemaVersion']['version'] keyring = VortaKeyring.get_keyring() if SCHEMA_VERSION < profile_schema: raise VersionException() elif SCHEMA_VERSION > profile_schema: # Add model upgrading code here, only needed if not adding columns if profile_schema < 16: for sourcedir in self._profile_dict['SourceFileModel']: sourcedir['dir_files_count'] = -1 sourcedir['dir_size'] = -1 sourcedir['path_isdir'] = False existing_profile = None if overwrite_profile: existing_profile = BackupProfileModel.get_or_none(BackupProfileModel.name == self.name) if existing_profile: self._profile_dict['id'] = existing_profile.id if not overwrite_profile or not existing_profile: # Guarantee uniqueness of ids while BackupProfileModel.get_or_none(BackupProfileModel.id == self.id) is not None: self._profile_dict['id'] += 1 # Add suffix incase names are the same if BackupProfileModel.get_or_none(BackupProfileModel.name == self.name) is not None: suffix = 1 while BackupProfileModel.get_or_none(BackupProfileModel.name == f"{self.name}-{suffix}") is not None: suffix += 1 self._profile_dict['name'] = f"{self.name}-{suffix}" # Load existing repo or restore it if self._profile_dict['repo']: repo = RepoModel.get_or_none(RepoModel.url == self.repo_url) if repo is None: # Load repo from export repo = dict_to_model(RepoModel, self._profile_dict['repo']) repo.save(force_insert=True) self._profile_dict['repo'] = model_to_dict(repo) if self.repo_password: keyring.set_password('vorta-repo', self.repo_url, self.repo_password) del self._profile_dict['password'] # Delete and recreate the tables to clear them if overwrite_settings: DB.drop_tables([SettingsModel, WifiSettingModel]) DB.create_tables([SettingsModel, WifiSettingModel]) SettingsModel.insert_many(self._profile_dict['SettingsModel']).execute() WifiSettingModel.insert_many(self._profile_dict['WifiSettingModel']).execute() # Set the profile ids to be match new profile for source in self._profile_dict['SourceFileModel']: source['profile'] = self.id SourceFileModel.insert_many(self._profile_dict['SourceFileModel']).execute() # Delete added dictionaries to make it match BackupProfileModel del self._profile_dict['SettingsModel'] del self._profile_dict['SourceFileModel'] del self._profile_dict['WifiSettingModel'] del self._profile_dict['SchemaVersion'] # dict to profile new_profile = dict_to_model(BackupProfileModel, self._profile_dict) if overwrite_profile and existing_profile: force_insert = False else: force_insert = True new_profile.save(force_insert=force_insert) init_db() # rerun db init code to perform the same operations on the new as as on application boot return new_profile @classmethod def from_json(cls, filename): with open(filename, 'r') as file: try: profile_export = ProfileExport(json.loads(file.read())) except JSONDecodeError as exception: raise ImportFailedException( 'This file does not contain valid JSON: {}'.format(str(exception)) ) from exception return profile_export def to_json(self): return json.dumps(self._profile_dict, default=self._converter, indent=4) @staticmethod def _converter(obj): if isinstance(obj, datetime.datetime): return obj.__str__() class VersionException(Exception): """For when current_version < export_version. Should only occur if downgrading""" pass class ImportFailedException(Exception): """Raised when a profile could not be imported.""" pass