diff --git a/lib/facts/backup-service.js b/lib/facts/backup-service.js new file mode 100644 --- /dev/null +++ b/lib/facts/backup-service.js @@ -0,0 +1,15 @@ +// @flow + +import { isDev } from '../utils/dev-utils.js'; + +type BackupServiceConfig = { + +url: string, +}; + +const config: BackupServiceConfig = { + url: isDev + ? 'https://backup.staging.commtechnologies.org' + : 'https://backup.commtechnologies.org', +}; + +export default config; diff --git a/web/database/utils/constants.js b/web/database/utils/constants.js --- a/web/database/utils/constants.js +++ b/web/database/utils/constants.js @@ -16,6 +16,8 @@ export const DEFAULT_OLM_FILENAME = 'olm.wasm'; export const COMM_SQLITE_DATABASE_PATH = 'comm.sqlite'; +export const COMM_SQLITE_BACKUP_RESTORE_DATABASE_PATH = + 'comm_backup_restore.sqlite'; export const NOTIFICATIONS_OLM_DATA_CONTENT = 'notificationsOlmDataContent'; diff --git a/web/database/worker/backup.js b/web/database/worker/backup.js new file mode 100644 --- /dev/null +++ b/web/database/worker/backup.js @@ -0,0 +1,54 @@ +// @flow + +import { BackupClient, RequestedData } from 'backup-client-wasm'; + +import backupService from 'lib/facts/backup-service.js'; +import type { AuthMetadata } from 'lib/shared/identity-client-context.js'; + +import { completeRootKey } from '../../redux/persist-constants.js'; +import type { EmscriptenModule } from '../types/module.js'; +import type { SQLiteQueryExecutor } from '../types/sqlite-query-executor.js'; +import { COMM_SQLITE_BACKUP_RESTORE_DATABASE_PATH } from '../utils/constants.js'; +import { importDatabaseContent } from '../utils/db-utils.js'; + +async function restoreBackup( + sqliteQueryExecutor: SQLiteQueryExecutor, + dbModule: EmscriptenModule, + authMetadata: AuthMetadata, + backupID: string, + backupDataKey: string, +) { + const { userID, deviceID, accessToken } = authMetadata; + if (!userID || !deviceID || !accessToken) { + throw new Error('Backup restore requires full authMetadata'); + } + const userIdentity = { userID, deviceID, accessToken }; + + const client = new BackupClient(backupService.url); + const result = await client.downloadBackupData( + { + type: 'BackupID', + backupID, + userIdentity, + }, + RequestedData.UserData, + ); + + importDatabaseContent( + result, + dbModule, + COMM_SQLITE_BACKUP_RESTORE_DATABASE_PATH, + ); + + const reduxPersistData = + sqliteQueryExecutor.getPersistStorageItem(completeRootKey); + + sqliteQueryExecutor.restoreFromMainCompaction( + COMM_SQLITE_BACKUP_RESTORE_DATABASE_PATH, + backupDataKey, + ); + + sqliteQueryExecutor.setPersistStorageItem(completeRootKey, reduxPersistData); +} + +export { restoreBackup }; diff --git a/web/database/worker/db-worker.js b/web/database/worker/db-worker.js --- a/web/database/worker/db-worker.js +++ b/web/database/worker/db-worker.js @@ -3,6 +3,7 @@ import initBackupClientModule from 'backup-client-wasm'; import localforage from 'localforage'; +import { restoreBackup } from './backup.js'; import { getClientStoreFromQueryExecutor, processDBStoreOperations, @@ -243,6 +244,14 @@ message.type === workerRequestMessageTypes.REMOVE_PERSIST_STORAGE_ITEM ) { sqliteQueryExecutor.removePersistStorageItem(message.key); + } else if (message.type === workerRequestMessageTypes.BACKUP_RESTORE) { + await restoreBackup( + sqliteQueryExecutor, + dbModule, + message.authMetadata, + message.backupID, + message.backupDataKey, + ); } persistNeeded = true; diff --git a/web/redux/persist-constants.js b/web/redux/persist-constants.js new file mode 100644 --- /dev/null +++ b/web/redux/persist-constants.js @@ -0,0 +1,7 @@ +// @flow + +const rootKey = 'root'; +const rootKeyPrefix = 'persist:'; +const completeRootKey = `${rootKeyPrefix}${rootKey}`; + +export { rootKey, rootKeyPrefix, completeRootKey }; diff --git a/web/redux/persist.js b/web/redux/persist.js --- a/web/redux/persist.js +++ b/web/redux/persist.js @@ -32,6 +32,7 @@ import commReduxStorageEngine from './comm-redux-storage-engine.js'; import { defaultWebState } from './default-state.js'; +import { rootKey, rootKeyPrefix } from './persist-constants.js'; import type { AppState } from './redux-setup.js'; import { nonUserSpecificFieldsWeb } from './redux-setup.js'; import { getDatabaseModule } from '../database/database-module-provider.js'; @@ -263,8 +264,6 @@ }, }; -const rootKey = 'root'; - const migrateStorageToSQLite: StorageMigrationFunction = async debug => { const databaseModule = await getDatabaseModule(); const isSupported = await databaseModule.isDatabaseSupported(); @@ -297,6 +296,7 @@ }; const persistConfig: PersistConfig = { + keyPrefix: rootKeyPrefix, key: rootKey, storage: commReduxStorageEngine, whitelist: isSQLiteSupported() diff --git a/web/types/worker-types.js b/web/types/worker-types.js --- a/web/types/worker-types.js +++ b/web/types/worker-types.js @@ -1,5 +1,6 @@ // @flow +import type { AuthMetadata } from 'lib/shared/identity-client-context'; import type { ClientDBStore, ClientDBStoreOperations, @@ -18,6 +19,7 @@ SET_PERSIST_STORAGE_ITEM: 8, REMOVE_PERSIST_STORAGE_ITEM: 9, CLEAR_SENSITIVE_DATA: 10, + BACKUP_RESTORE: 11, }); export const workerWriteRequests: $ReadOnlyArray = [ @@ -25,6 +27,7 @@ workerRequestMessageTypes.SET_CURRENT_USER_ID, workerRequestMessageTypes.SET_PERSIST_STORAGE_ITEM, workerRequestMessageTypes.REMOVE_PERSIST_STORAGE_ITEM, + workerRequestMessageTypes.BACKUP_RESTORE, ]; export type PingWorkerRequestMessage = { @@ -82,6 +85,13 @@ +type: 10, }; +export type BackupRestoreRequestMessage = { + +type: 11, + +authMetadata: AuthMetadata, + +backupID: string, + +backupDataKey: string, +}; + export type WorkerRequestMessage = | PingWorkerRequestMessage | InitWorkerRequestMessage @@ -93,7 +103,8 @@ | GetPersistStorageItemRequestMessage | SetPersistStorageItemRequestMessage | RemovePersistStorageItemRequestMessage - | ClearSensitiveDataRequestMessage; + | ClearSensitiveDataRequestMessage + | BackupRestoreRequestMessage; export type WorkerRequestProxyMessage = { +id: number,