Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F32216611
D14885.1765168022.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
17 KB
Referenced Files
None
Subscribers
None
D14885.1765168022.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D14885: [lib] Track UserData restore and make it resumable
Attached
Detach File
Event Timeline
Log In to Comment