diff --git a/lib/backup/persist-shared-migrations.js b/lib/backup/persist-shared-migrations.js new file mode 100644 --- /dev/null +++ b/lib/backup/persist-shared-migrations.js @@ -0,0 +1,15 @@ +// @flow + +import type { DatabaseIdentifier } from '../types/database-identifier-types.js'; +import type { StoreOperations } from '../types/store-ops-types.js'; + +export type SharedMigrationFunction = ( + databaseIdentifier: DatabaseIdentifier, +) => Promise; + +export type SharedMigrationsManifest = { + +[number | string]: SharedMigrationFunction, +}; +const sharedMigrations: SharedMigrationsManifest = {}; + +export { sharedMigrations }; diff --git a/lib/backup/restored-migrations.js b/lib/backup/restored-migrations.js new file mode 100644 --- /dev/null +++ b/lib/backup/restored-migrations.js @@ -0,0 +1,86 @@ +// @flow + +import { sharedMigrations } from './persist-shared-migrations.js'; +import { databaseIdentifier } from '../types/database-identifier-types.js'; +import { syncedMetadataNames } from '../types/synced-metadata-types.js'; +import { getConfig } from '../utils/config.js'; +import { getMessageForException } from '../utils/errors.js'; + +async function getRestoredStoreVersion(): Promise { + const { sqliteAPI } = getConfig(); + const clientStore = await sqliteAPI.getClientDBStore( + databaseIdentifier.RESTORED, + ); + const syncedMetadata = clientStore.syncedMetadata; + const storeVersion = syncedMetadata?.[syncedMetadataNames.DB_VERSION]; + if (!storeVersion) { + throw new Error('storeVersion is missing'); + } + return parseInt(storeVersion, 10); +} + +async function runRestoredBackupMigrations() { + const currentStoreVersion = getConfig().platformDetails.stateVersion; + if (!currentStoreVersion) { + throw new Error('currentStoreVersion is missing'); + } + + const restoredStoreVersion = await getRestoredStoreVersion(); + if (restoredStoreVersion === currentStoreVersion) { + console.log('backup-restore: versions match, noop migration'); + return; + } + + if (restoredStoreVersion > currentStoreVersion) { + console.log('backup-restore: current app is older than backup'); + throw new Error('app out-of-date'); + } + + console.log( + `backup-restore: migrating from ${restoredStoreVersion} to ${currentStoreVersion}`, + ); + + const migrationKeys = Object.keys(sharedMigrations) + .map(ver => parseInt(ver)) + .filter(key => currentStoreVersion >= key && key > restoredStoreVersion) + .sort((a, b) => a - b); + + for (const versionKey of migrationKeys) { + console.log('backup-restore: running migration for versionKey', versionKey); + + if (!versionKey) { + continue; + } + try { + const ops = await sharedMigrations[versionKey]( + databaseIdentifier.RESTORED, + ); + const versionUpdateOp = { + type: 'replace_synced_metadata_entry', + payload: { + name: syncedMetadataNames.DB_VERSION, + data: versionKey.toString(), + }, + }; + const dbOps = { + ...ops, + syncedMetadataStoreOperations: [ + ...(ops.syncedMetadataStoreOperations ?? []), + versionUpdateOp, + ], + }; + await getConfig().sqliteAPI.processDBStoreOperations( + dbOps, + databaseIdentifier.RESTORED, + ); + } catch (exception) { + throw new Error( + `Error while running migration: ${versionKey}: ${ + getMessageForException(exception) ?? 'unknown error' + }`, + ); + } + } +} + +export { runRestoredBackupMigrations }; diff --git a/lib/components/secondary-device-qr-auth-context-provider.react.js b/lib/components/secondary-device-qr-auth-context-provider.react.js --- a/lib/components/secondary-device-qr-auth-context-provider.react.js +++ b/lib/components/secondary-device-qr-auth-context-provider.react.js @@ -5,6 +5,7 @@ import { useDebugLogs } from './debug-logs-context.js'; import { setClientDBStoreActionType } from '../actions/client-db-store-actions.js'; +import { runRestoredBackupMigrations } from '../backup/restored-migrations.js'; import { qrCodeLinkURL } from '../facts/links.js'; import { useSecondaryDeviceLogIn } from '../hooks/login-hooks.js'; import { uintArrayToHexString } from '../media/data-utils.js'; @@ -202,6 +203,7 @@ } await sqliteAPI.restoreUserData(backupData, identityAuthResult); await sqliteAPI.migrateBackupSchema(); + await runRestoredBackupMigrations(); await sqliteAPI.copyContentFromBackupDatabase(); const clientDBStore = await sqliteAPI.getClientDBStore( databaseIdentifier.MAIN, diff --git a/native/account/restore.js b/native/account/restore.js --- a/native/account/restore.js +++ b/native/account/restore.js @@ -8,6 +8,7 @@ restoreUserActionTypes, type RestoreUserResult, } from 'lib/actions/user-actions.js'; +import { runRestoredBackupMigrations } from 'lib/backup/restored-migrations.js'; import { useDebugLogs } from 'lib/components/debug-logs-context.js'; import { useLogIn, @@ -211,6 +212,7 @@ } const backupData = await commCoreModule.getQRAuthBackupData(); await sqliteAPI.migrateBackupSchema(); + await runRestoredBackupMigrations(); await sqliteAPI.copyContentFromBackupDatabase(); await sqliteAPI.restoreUserData(backupData, identityAuthResult);