Page MenuHomePhorge

D14885.1765168022.diff
No OneTemporary

Size
17 KB
Referenced Files
None
Subscribers
None

D14885.1765168022.diff

diff --git a/lib/actions/backup-actions.js b/lib/actions/backup-actions.js
--- a/lib/actions/backup-actions.js
+++ b/lib/actions/backup-actions.js
@@ -19,4 +19,14 @@
+latestDatabaseVersion: number,
};
-export { createUserKeysBackupActionTypes, createUserDataBackupActionTypes };
+const restoreUserDataStepActionTypes = Object.freeze({
+ started: 'RESTORE_USER_DATA_STEP_STARTED',
+ success: 'RESTORE_USER_DATA_STEP_SUCCESS',
+ failed: 'RESTORE_USER_DATA_STEP_FAILED',
+});
+
+export {
+ createUserKeysBackupActionTypes,
+ createUserDataBackupActionTypes,
+ restoreUserDataStepActionTypes,
+};
diff --git a/lib/backup/use-user-data-restore.js b/lib/backup/use-user-data-restore.js
--- a/lib/backup/use-user-data-restore.js
+++ b/lib/backup/use-user-data-restore.js
@@ -1,23 +1,32 @@
// @flow
+import invariant from 'invariant';
import * as React from 'react';
import { runRestoredBackupMigrations } from './restored-migrations.js';
+import { restoreUserDataStepActionTypes } from '../actions/backup-actions.js';
import { setClientDBStoreActionType } from '../actions/client-db-store-actions.js';
import { logTypes, useDebugLogs } from '../components/debug-logs-context.js';
+import {
+ restoreUserDataSteps,
+ type RestoreUserDataStep,
+} from '../types/backup-types.js';
import { databaseIdentifier } from '../types/database-identifier-types.js';
import type { IdentityAuthResult } from '../types/identity-service-types.js';
import { syncedMetadataNames } from '../types/synced-metadata-types.js';
import type { QRAuthBackupData } from '../types/tunnelbroker/qr-code-auth-message-types.js';
import { getConfig } from '../utils/config.js';
-import { useDispatch } from '../utils/redux-utils.js';
+import { useDispatchActionPromise } from '../utils/redux-promise-utils.js';
+import { useSelector, useDispatch } from '../utils/redux-utils.js';
function useUserDataRestore(): (
backupData: QRAuthBackupData,
identityAuthResult: IdentityAuthResult,
) => Promise<void> {
const dispatch = useDispatch();
+ const dispatchActionPromise = useDispatchActionPromise();
const { addLog } = useDebugLogs();
+ const restoreBackupState = useSelector(state => state.restoreBackupState);
return React.useCallback(
async (
@@ -26,91 +35,153 @@
) => {
const { sqliteAPI } = getConfig();
- // 1. Download database and apply all logs
- await sqliteAPI.restoreUserData(backupData, identityAuthResult);
+ // Determine starting step based on current state
+ const stepOrder: $ReadOnlyArray<RestoreUserDataStep> = [
+ restoreUserDataSteps.RESTORE_DATABASE,
+ restoreUserDataSteps.MIGRATE_BACKUP_SCHEMA,
+ restoreUserDataSteps.RUN_RESTORED_BACKUP_MIGRATIONS,
+ restoreUserDataSteps.COPY_CONTENT_FROM_BACKUP_DB,
+ ];
+ let startStepIndex = 0;
- // 2. Check database versions and migrate if needed
- const [mainDatabaseVersion, restoredDatabaseVersion] = await Promise.all([
- sqliteAPI.getDatabaseVersion(databaseIdentifier.MAIN),
- sqliteAPI.getDatabaseVersion(databaseIdentifier.RESTORED),
- ]);
-
- if (mainDatabaseVersion === restoredDatabaseVersion) {
- addLog(
- 'User Data Restore',
- `Main and restored database versions are equal: ` +
- `${mainDatabaseVersion}, skipping schema migrations.`,
- new Set([logTypes.BACKUP]),
- );
- } else if (mainDatabaseVersion > restoredDatabaseVersion) {
- addLog(
- 'User Data Restore',
- `Main database version (${mainDatabaseVersion}) is higher ` +
- `than restored database version (${restoredDatabaseVersion}), ` +
- `migrating schema.`,
- new Set([logTypes.BACKUP]),
+ if (restoreBackupState.status === 'user_data_restore_step_completed') {
+ const completedStepIndex = stepOrder.indexOf(
+ restoreBackupState.payload.step,
);
- await sqliteAPI.migrateBackupSchema();
- } else if (mainDatabaseVersion < restoredDatabaseVersion) {
- addLog(
- 'User Data Restore',
- `Main database version (${mainDatabaseVersion}) is lower ` +
- `than restored database version (${restoredDatabaseVersion}), ` +
- ` aborting.`,
- new Set([logTypes.BACKUP, logTypes.ERROR]),
- );
- throw new Error('backup_is_newer');
+ startStepIndex = completedStepIndex + 1;
+ } else if (restoreBackupState.status === 'user_data_restore_started') {
+ startStepIndex = stepOrder.indexOf(restoreBackupState.payload.step);
+ } else {
+ // for any other state, start from scratch
+ startStepIndex = 0;
}
- // 3. Check store versions and migrate if needed
- const mainStoreVersion = getConfig().platformDetails.stateVersion;
- const restoredStoreVersionString = await sqliteAPI.getSyncedMetadata(
- syncedMetadataNames.STORE_VERSION,
- databaseIdentifier.RESTORED,
- );
+ // if all steps from last restore succeeded, start from scratch
+ if (startStepIndex > 3) {
+ startStepIndex = 0;
+ }
+
+ invariant(startStepIndex >= 0, 'invalid UserData restore step');
- if (!mainStoreVersion || !restoredStoreVersionString) {
- addLog(
- 'User Data Restore',
- `Error when restoring user data, main store version(${
- mainStoreVersion ?? 'undefined'
- }) or restored store version (${
- restoredStoreVersionString ?? 'undefined'
- }) are undefined`,
- new Set([logTypes.BACKUP, logTypes.ERROR]),
+ // 1. Download database and apply all logs
+ if (startStepIndex === 0) {
+ await dispatchActionPromise(
+ restoreUserDataStepActionTypes,
+ sqliteAPI.restoreUserData(backupData, identityAuthResult),
+ undefined,
+ { step: 'restore_database' },
);
- return;
}
- const restoredStoreVersion = parseInt(restoredStoreVersionString);
+ // 2. Check database versions and migrate if needed
+ if (startStepIndex <= 1) {
+ await dispatchActionPromise(
+ restoreUserDataStepActionTypes,
+ (async () => {
+ const [mainDatabaseVersion, restoredDatabaseVersion] =
+ await Promise.all([
+ sqliteAPI.getDatabaseVersion(databaseIdentifier.MAIN),
+ sqliteAPI.getDatabaseVersion(databaseIdentifier.RESTORED),
+ ]);
- if (mainStoreVersion === restoredStoreVersion) {
- addLog(
- 'User Data Restore',
- `Main and restored store versions are equal: ${mainStoreVersion}, ` +
- `skipping data migrations`,
- new Set([logTypes.BACKUP]),
+ if (mainDatabaseVersion === restoredDatabaseVersion) {
+ addLog(
+ 'User Data Restore',
+ `Main and restored database versions are equal: ` +
+ `${mainDatabaseVersion}, skipping schema migrations.`,
+ new Set([logTypes.BACKUP]),
+ );
+ } else if (mainDatabaseVersion > restoredDatabaseVersion) {
+ addLog(
+ 'User Data Restore',
+ `Main database version (${mainDatabaseVersion}) is higher ` +
+ `than restored database version (${restoredDatabaseVersion}), ` +
+ `migrating schema.`,
+ new Set([logTypes.BACKUP]),
+ );
+ await sqliteAPI.migrateBackupSchema();
+ } else if (mainDatabaseVersion < restoredDatabaseVersion) {
+ addLog(
+ 'User Data Restore',
+ `Main database version (${mainDatabaseVersion}) is lower ` +
+ `than restored database version (${restoredDatabaseVersion}), ` +
+ ` aborting.`,
+ new Set([logTypes.BACKUP, logTypes.ERROR]),
+ );
+ throw new Error('backup_is_newer');
+ }
+ })(),
+ undefined,
+ { step: 'migrate_backup_schema' },
);
- } else if (mainStoreVersion > restoredStoreVersion) {
- addLog(
- 'User Data Restore',
- `Main store version (${mainStoreVersion}) is higher than ` +
- `restored store version (${restoredStoreVersion}), migrating data`,
- new Set([logTypes.BACKUP]),
- );
- await runRestoredBackupMigrations();
- } else if (mainStoreVersion < restoredStoreVersion) {
- addLog(
- 'User Data Restore',
- `Main store version (${mainStoreVersion}) is lower than ` +
- `restored store version (${restoredStoreVersion}), aborting`,
- new Set([logTypes.BACKUP, logTypes.ERROR]),
+ }
+
+ // 3. Check store versions and migrate if needed
+ if (startStepIndex <= 2) {
+ await dispatchActionPromise(
+ restoreUserDataStepActionTypes,
+ (async () => {
+ const mainStoreVersion = getConfig().platformDetails.stateVersion;
+ const restoredStoreVersionString =
+ await sqliteAPI.getSyncedMetadata(
+ syncedMetadataNames.STORE_VERSION,
+ databaseIdentifier.RESTORED,
+ );
+
+ if (!mainStoreVersion || !restoredStoreVersionString) {
+ addLog(
+ 'User Data Restore',
+ `Error when restoring user data, main store version(${
+ mainStoreVersion ?? 'undefined'
+ }) or restored store version (${
+ restoredStoreVersionString ?? 'undefined'
+ }) are undefined`,
+ new Set([logTypes.BACKUP, logTypes.ERROR]),
+ );
+ throw new Error('version_check_failed');
+ }
+
+ const restoredStoreVersion = parseInt(restoredStoreVersionString);
+
+ if (mainStoreVersion === restoredStoreVersion) {
+ addLog(
+ 'User Data Restore',
+ `Main and restored store versions are equal: ${mainStoreVersion}, ` +
+ `skipping data migrations`,
+ new Set([logTypes.BACKUP]),
+ );
+ } else if (mainStoreVersion > restoredStoreVersion) {
+ addLog(
+ 'User Data Restore',
+ `Main store version (${mainStoreVersion}) is higher than ` +
+ `restored store version (${restoredStoreVersion}), migrating data`,
+ new Set([logTypes.BACKUP]),
+ );
+ await runRestoredBackupMigrations();
+ } else if (mainStoreVersion < restoredStoreVersion) {
+ addLog(
+ 'User Data Restore',
+ `Main store version (${mainStoreVersion}) is lower than ` +
+ `restored store version (${restoredStoreVersion}), aborting`,
+ new Set([logTypes.BACKUP, logTypes.ERROR]),
+ );
+ throw new Error('backup_is_newer');
+ }
+ })(),
+ undefined,
+ { step: 'run_restored_backup_migrations' },
);
- throw new Error('backup_is_newer');
}
// 4. Copy content to main database
- await sqliteAPI.copyContentFromBackupDatabase();
+ if (startStepIndex <= 3) {
+ await dispatchActionPromise(
+ restoreUserDataStepActionTypes,
+ sqliteAPI.copyContentFromBackupDatabase(),
+ undefined,
+ { step: 'copy_content_from_backup_db' },
+ );
+ }
// 5. Populate store
const clientDBStore = await sqliteAPI.getClientDBStore(
@@ -123,7 +194,7 @@
payload: clientDBStore,
});
},
- [addLog, dispatch],
+ [addLog, dispatch, dispatchActionPromise, restoreBackupState],
);
}
diff --git a/lib/reducers/backup-reducer.js b/lib/reducers/backup-reducer.js
--- a/lib/reducers/backup-reducer.js
+++ b/lib/reducers/backup-reducer.js
@@ -5,12 +5,18 @@
import {
createUserDataBackupActionTypes,
createUserKeysBackupActionTypes,
+ restoreUserDataStepActionTypes,
} from '../actions/backup-actions.js';
import {
changeIdentityUserPasswordActionTypes,
restoreUserActionTypes,
} from '../actions/user-actions.js';
-import type { BackupStore, RestoreBackupState } from '../types/backup-types.js';
+import {
+ restoreUserDataSteps,
+ type BackupStore,
+ type RestoreBackupState,
+ type RestoreUserDataStep,
+} from '../types/backup-types.js';
import type { BaseAction } from '../types/redux-types.js';
import { fullBackupSupport } from '../utils/services-utils.js';
@@ -90,7 +96,78 @@
return { ...store, status: 'user_data_backup_failed' };
}
+ // restoreUserDataStepActionTypes
+ if (action.type === restoreUserDataStepActionTypes.started) {
+ const { step } = action.payload;
+ validateUserDataRestoreStepOrder(store, step);
+
+ return {
+ status: 'user_data_restore_started',
+ payload: { step },
+ };
+ } else if (action.type === restoreUserDataStepActionTypes.success) {
+ // For success actions, we need to get the current step from the store state
+ if (store.status === 'user_data_restore_started') {
+ const { step } = store.payload;
+ // If this was the last step, mark restore as completed
+ if (step === 'copy_content_from_backup_db') {
+ return {
+ status: 'user_data_restore_completed',
+ payload: {},
+ };
+ }
+ return {
+ status: 'user_data_restore_step_completed',
+ payload: { step },
+ };
+ }
+ return store;
+ } else if (action.type === restoreUserDataStepActionTypes.failed) {
+ // For failed actions, we need to get the current step from the store state
+ if (store.status === 'user_data_restore_started') {
+ const { payload: error } = action;
+ const { step } = store.payload;
+ return {
+ status: 'user_data_restore_failed',
+ payload: { step, error },
+ };
+ }
+ return store;
+ }
+
return store;
}
+const stepOrder: $ReadOnlyArray<RestoreUserDataStep> = [
+ restoreUserDataSteps.RESTORE_DATABASE,
+ restoreUserDataSteps.MIGRATE_BACKUP_SCHEMA,
+ restoreUserDataSteps.RUN_RESTORED_BACKUP_MIGRATIONS,
+ restoreUserDataSteps.COPY_CONTENT_FROM_BACKUP_DB,
+];
+function validateUserDataRestoreStepOrder(
+ store: RestoreBackupState,
+ step: RestoreUserDataStep,
+) {
+ const currentStepIndex = stepOrder.indexOf(step);
+
+ if (store.status === 'user_data_restore_step_completed') {
+ const lastCompletedStepIndex = stepOrder.indexOf(store.payload.step);
+ invariant(
+ currentStepIndex === lastCompletedStepIndex + 1,
+ `Invalid step order: trying to start '${step}' but last completed step was '${store.payload.step}'`,
+ );
+ } else if (store.status === 'user_data_restore_started') {
+ invariant(
+ currentStepIndex === stepOrder.indexOf(store.payload.step),
+ `Invalid step: trying to restart '${step}' but current step is '${store.payload.step}'`,
+ );
+ } else {
+ // Starting fresh - should always start with first step
+ invariant(
+ step === 'restore_database',
+ `Invalid starting step: expected 'restore_database' but got '${step}'`,
+ );
+ }
+}
+
export { reduceBackupStore, reduceRestoreBackupState };
diff --git a/lib/types/backup-types.js b/lib/types/backup-types.js
--- a/lib/types/backup-types.js
+++ b/lib/types/backup-types.js
@@ -82,6 +82,14 @@
+latestDatabaseVersion?: number,
};
+export const restoreUserDataSteps = Object.freeze({
+ RESTORE_DATABASE: 'restore_database',
+ MIGRATE_BACKUP_SCHEMA: 'migrate_backup_schema',
+ RUN_RESTORED_BACKUP_MIGRATIONS: 'run_restored_backup_migrations',
+ COPY_CONTENT_FROM_BACKUP_DB: 'copy_content_from_backup_db',
+});
+export type RestoreUserDataStep = $Values<typeof restoreUserDataSteps>;
+
export type RestoreBackupState =
| {
status: 'no_backup',
@@ -110,4 +118,27 @@
| {
status: 'user_data_backup_failed',
payload: {},
+ }
+ | {
+ status: 'user_data_restore_started',
+ payload: {
+ step: RestoreUserDataStep,
+ },
+ }
+ | {
+ status: 'user_data_restore_step_completed',
+ payload: {
+ step: RestoreUserDataStep,
+ },
+ }
+ | {
+ status: 'user_data_restore_failed',
+ payload: {
+ step: RestoreUserDataStep,
+ error: Error,
+ },
+ }
+ | {
+ status: 'user_data_restore_completed',
+ payload: {},
};
diff --git a/lib/types/redux-types.js b/lib/types/redux-types.js
--- a/lib/types/redux-types.js
+++ b/lib/types/redux-types.js
@@ -32,6 +32,7 @@
import type {
LocalLatestBackupInfo,
RestoreBackupState,
+ RestoreUserDataStep,
} from './backup-types.js';
import type {
CommunityStore,
@@ -1646,6 +1647,22 @@
+payload: Error,
+loadingInfo: LoadingInfo,
}
+ | {
+ +type: 'RESTORE_USER_DATA_STEP_STARTED',
+ +loadingInfo: LoadingInfo,
+ +payload: { +step: RestoreUserDataStep },
+ }
+ | {
+ +type: 'RESTORE_USER_DATA_STEP_SUCCESS',
+ +payload?: void,
+ +loadingInfo: LoadingInfo,
+ }
+ | {
+ +type: 'RESTORE_USER_DATA_STEP_FAILED',
+ +error: true,
+ +payload: Error,
+ +loadingInfo: LoadingInfo,
+ }
| {
+type: 'RESTORE_USER_STARTED',
+loadingInfo: LoadingInfo,

File Metadata

Mime Type
text/plain
Expires
Mon, Dec 8, 4:27 AM (9 h, 20 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5847056
Default Alt Text
D14885.1765168022.diff (17 KB)

Event Timeline