Handle fatal error during backup import
This commit is contained in:
parent
94dba11bcb
commit
10eeb63776
9 changed files with 146 additions and 41 deletions
|
@ -4791,6 +4791,10 @@
|
||||||
"messageformat": "Your messages could not be transferred. Check your internet connection and try again.",
|
"messageformat": "Your messages could not be transferred. Check your internet connection and try again.",
|
||||||
"description": "Body of the error modal in the backup import screen"
|
"description": "Body of the error modal in the backup import screen"
|
||||||
},
|
},
|
||||||
|
"icu:BackupImportScreen__error-fatal__body": {
|
||||||
|
"messageformat": "Your messages could not be transferred due to an error. Try again by re-linking this desktop.",
|
||||||
|
"description": "Body of the error modal in the backup import screen if we have a non-retriable error."
|
||||||
|
},
|
||||||
"icu:BackupImportScreen__error__confirm": {
|
"icu:BackupImportScreen__error__confirm": {
|
||||||
"messageformat": "Retry",
|
"messageformat": "Retry",
|
||||||
"description": "Text of the retry button of the error modal in the backup import screen"
|
"description": "Text of the retry button of the error modal in the backup import screen"
|
||||||
|
|
|
@ -1638,24 +1638,29 @@ export async function startApp(): Promise<void> {
|
||||||
onOffline();
|
onOffline();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download backup before enabling request handler and storage service
|
const backupDownloadPath = window.storage.get('backupDownloadPath');
|
||||||
try {
|
if (backupDownloadPath) {
|
||||||
await backupsService.download({
|
// Download backup before enabling request handler and storage service
|
||||||
onProgress: (backupStep, currentBytes, totalBytes) => {
|
try {
|
||||||
window.reduxActions.installer.updateBackupImportProgress({
|
await backupsService.downloadAndImport({
|
||||||
backupStep,
|
onProgress: (backupStep, currentBytes, totalBytes) => {
|
||||||
currentBytes,
|
window.reduxActions.installer.updateBackupImportProgress({
|
||||||
totalBytes,
|
backupStep,
|
||||||
});
|
currentBytes,
|
||||||
},
|
totalBytes,
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
log.info('afterStart: backup downloaded, resolving');
|
log.info('afterStart: backup downloaded, resolving');
|
||||||
|
backupReady.resolve();
|
||||||
|
} catch (error) {
|
||||||
|
log.error('afterStart: backup download failed, rejecting');
|
||||||
|
backupReady.reject(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
backupReady.resolve();
|
backupReady.resolve();
|
||||||
} catch (error) {
|
|
||||||
log.error('afterStart: backup download failed, rejecting');
|
|
||||||
backupReady.reject(error);
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server.registerRequestHandler(messageReceiver);
|
server.registerRequestHandler(messageReceiver);
|
||||||
|
|
|
@ -39,6 +39,7 @@ export type OwnProps = Readonly<{
|
||||||
hasXButton?: boolean;
|
hasXButton?: boolean;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
moduleClassName?: string;
|
moduleClassName?: string;
|
||||||
|
noEscapeClose?: boolean;
|
||||||
noMouseClose?: boolean;
|
noMouseClose?: boolean;
|
||||||
noDefaultCancelButton?: boolean;
|
noDefaultCancelButton?: boolean;
|
||||||
onCancel?: () => unknown;
|
onCancel?: () => unknown;
|
||||||
|
@ -80,6 +81,7 @@ export const ConfirmationDialog = React.memo(function ConfirmationDialogInner({
|
||||||
i18n,
|
i18n,
|
||||||
isSpinning,
|
isSpinning,
|
||||||
moduleClassName,
|
moduleClassName,
|
||||||
|
noEscapeClose,
|
||||||
noMouseClose,
|
noMouseClose,
|
||||||
noDefaultCancelButton,
|
noDefaultCancelButton,
|
||||||
onCancel,
|
onCancel,
|
||||||
|
@ -163,6 +165,7 @@ export const ConfirmationDialog = React.memo(function ConfirmationDialogInner({
|
||||||
<ModalHost
|
<ModalHost
|
||||||
modalName={modalName}
|
modalName={modalName}
|
||||||
noMouseClose={noMouseClose}
|
noMouseClose={noMouseClose}
|
||||||
|
noEscapeClose={noEscapeClose}
|
||||||
onClose={close}
|
onClose={close}
|
||||||
onEscape={cancelAndClose}
|
onEscape={cancelAndClose}
|
||||||
onTopOfEverything={onTopOfEverything}
|
onTopOfEverything={onTopOfEverything}
|
||||||
|
|
|
@ -99,7 +99,15 @@ Error.args = {
|
||||||
backupStep: InstallScreenBackupStep.Download,
|
backupStep: InstallScreenBackupStep.Download,
|
||||||
currentBytes: 500 * 1024,
|
currentBytes: 500 * 1024,
|
||||||
totalBytes: 1024 * 1024,
|
totalBytes: 1024 * 1024,
|
||||||
error: InstallScreenBackupError.Unknown,
|
error: InstallScreenBackupError.Retriable,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FatalError = Template.bind({});
|
||||||
|
FatalError.args = {
|
||||||
|
backupStep: InstallScreenBackupStep.Process,
|
||||||
|
currentBytes: 500 * 1024,
|
||||||
|
totalBytes: 1024 * 1024,
|
||||||
|
error: InstallScreenBackupError.Fatal,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UnsupportedVersion = Template.bind({});
|
export const UnsupportedVersion = Template.bind({});
|
||||||
|
|
|
@ -32,6 +32,7 @@ export type PropsType = Readonly<{
|
||||||
error?: InstallScreenBackupError;
|
error?: InstallScreenBackupError;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onRetry: () => void;
|
onRetry: () => void;
|
||||||
|
onRestartLink: () => void;
|
||||||
|
|
||||||
// Updater UI
|
// Updater UI
|
||||||
updates: UpdatesStateType;
|
updates: UpdatesStateType;
|
||||||
|
@ -49,6 +50,7 @@ export function InstallScreenBackupImportStep({
|
||||||
error,
|
error,
|
||||||
onCancel,
|
onCancel,
|
||||||
onRetry,
|
onRetry,
|
||||||
|
onRestartLink,
|
||||||
|
|
||||||
updates,
|
updates,
|
||||||
currentVersion,
|
currentVersion,
|
||||||
|
@ -158,7 +160,7 @@ export function InstallScreenBackupImportStep({
|
||||||
OS={OS}
|
OS={OS}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (error === InstallScreenBackupError.Unknown) {
|
} else if (error === InstallScreenBackupError.Retriable) {
|
||||||
if (!isConfirmingSkip) {
|
if (!isConfirmingSkip) {
|
||||||
errorElem = (
|
errorElem = (
|
||||||
<ConfirmationDialog
|
<ConfirmationDialog
|
||||||
|
@ -179,6 +181,27 @@ export function InstallScreenBackupImportStep({
|
||||||
</ConfirmationDialog>
|
</ConfirmationDialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} else if (error === InstallScreenBackupError.Fatal) {
|
||||||
|
errorElem = (
|
||||||
|
<ConfirmationDialog
|
||||||
|
dialogName="InstallScreenBackupImportStep.error"
|
||||||
|
title={i18n('icu:BackupImportScreen__error__title')}
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
action: onRestartLink,
|
||||||
|
style: 'affirmative',
|
||||||
|
text: i18n('icu:BackupImportScreen__error__confirm'),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
i18n={i18n}
|
||||||
|
onClose={() => null}
|
||||||
|
noMouseClose
|
||||||
|
noDefaultCancelButton
|
||||||
|
noEscapeClose
|
||||||
|
>
|
||||||
|
{i18n('icu:BackupImportScreen__error-fatal__body')}
|
||||||
|
</ConfirmationDialog>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
throw missingCaseError(error);
|
throw missingCaseError(error);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
// Copyright 2024 Signal Messenger, LLC
|
// Copyright 2024 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
|
|
||||||
import type Long from 'long';
|
import type Long from 'long';
|
||||||
|
|
||||||
|
@ -8,3 +9,7 @@ export class UnsupportedBackupVersion extends Error {
|
||||||
super(`Unsupported backup version: ${version}`);
|
super(`Unsupported backup version: ${version}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class BackupDownloadFailedError extends Error {}
|
||||||
|
|
||||||
|
export class BackupProcessingError extends Error {}
|
||||||
|
|
|
@ -49,7 +49,11 @@ import { BackupCredentials } from './credentials';
|
||||||
import { BackupAPI } from './api';
|
import { BackupAPI } from './api';
|
||||||
import { validateBackup } from './validator';
|
import { validateBackup } from './validator';
|
||||||
import { BackupType } from './types';
|
import { BackupType } from './types';
|
||||||
import { UnsupportedBackupVersion } from './errors';
|
import {
|
||||||
|
BackupDownloadFailedError,
|
||||||
|
BackupProcessingError,
|
||||||
|
UnsupportedBackupVersion,
|
||||||
|
} from './errors';
|
||||||
import { ToastType } from '../../types/Toast';
|
import { ToastType } from '../../types/Toast';
|
||||||
import { isNightly } from '../../util/version';
|
import { isNightly } from '../../util/version';
|
||||||
|
|
||||||
|
@ -117,14 +121,14 @@ export class BackupsService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async download(options: DownloadOptionsType): Promise<void> {
|
public async downloadAndImport(options: DownloadOptionsType): Promise<void> {
|
||||||
const backupDownloadPath = window.storage.get('backupDownloadPath');
|
const backupDownloadPath = window.storage.get('backupDownloadPath');
|
||||||
if (!backupDownloadPath) {
|
if (!backupDownloadPath) {
|
||||||
log.warn('backups.download: no backup download path, skipping');
|
log.warn('backups.downloadAndImport: no backup download path, skipping');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info('backups.download: downloading...');
|
log.info('backups.downloadAndImport: downloading...');
|
||||||
|
|
||||||
const ephemeralKey = window.storage.get('backupEphemeralKey');
|
const ephemeralKey = window.storage.get('backupEphemeralKey');
|
||||||
|
|
||||||
|
@ -136,22 +140,66 @@ export class BackupsService {
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
hasBackup = await this.doDownload({
|
hasBackup = await this.doDownloadAndImport({
|
||||||
downloadPath: absoluteDownloadPath,
|
downloadPath: absoluteDownloadPath,
|
||||||
onProgress: options.onProgress,
|
onProgress: options.onProgress,
|
||||||
ephemeralKey,
|
ephemeralKey,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.warn(
|
|
||||||
'backups.download: error, prompting user to retry',
|
|
||||||
Errors.toLogFormat(error)
|
|
||||||
);
|
|
||||||
this.downloadRetryPromise = explodePromise<RetryBackupImportValue>();
|
this.downloadRetryPromise = explodePromise<RetryBackupImportValue>();
|
||||||
|
|
||||||
|
let installerError: InstallScreenBackupError;
|
||||||
|
if (error instanceof UnsupportedBackupVersion) {
|
||||||
|
installerError = InstallScreenBackupError.UnsupportedVersion;
|
||||||
|
log.error(
|
||||||
|
'backups.downloadAndImport: unsupported version',
|
||||||
|
Errors.toLogFormat(error)
|
||||||
|
);
|
||||||
|
} else if (error instanceof BackupDownloadFailedError) {
|
||||||
|
installerError = InstallScreenBackupError.Retriable;
|
||||||
|
log.warn(
|
||||||
|
'backups.downloadAndImport: download error, prompting user to retry',
|
||||||
|
Errors.toLogFormat(error)
|
||||||
|
);
|
||||||
|
} else if (error instanceof BackupProcessingError) {
|
||||||
|
installerError = InstallScreenBackupError.Fatal;
|
||||||
|
log.error(
|
||||||
|
'backups.downloadAndImport: fatal error during processing; unlinking & deleting data',
|
||||||
|
Errors.toLogFormat(error)
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await window.textsecure.server?.unlink();
|
||||||
|
} catch (e) {
|
||||||
|
log.warn(
|
||||||
|
'Error while unlinking; this may be expected for the unlink operation',
|
||||||
|
Errors.toLogFormat(e)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
log.info('backups.downloadAndImport: deleting all data');
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await window.textsecure.storage.protocol.removeAllData();
|
||||||
|
log.info(
|
||||||
|
'backups.downloadAndImport: all data deleted successfully'
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
log.error(
|
||||||
|
'backups.downloadAndImport: unable to remove all data',
|
||||||
|
Errors.toLogFormat(e)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.error(
|
||||||
|
'backups.downloadAndImport: unknown error, prompting user to retry'
|
||||||
|
);
|
||||||
|
installerError = InstallScreenBackupError.Retriable;
|
||||||
|
}
|
||||||
|
|
||||||
window.reduxActions.installer.updateBackupImportProgress({
|
window.reduxActions.installer.updateBackupImportProgress({
|
||||||
error:
|
error: installerError,
|
||||||
error instanceof UnsupportedBackupVersion
|
|
||||||
? InstallScreenBackupError.UnsupportedVersion
|
|
||||||
: InstallScreenBackupError.Unknown,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
@ -174,7 +222,7 @@ export class BackupsService {
|
||||||
await window.storage.remove('backupEphemeralKey');
|
await window.storage.remove('backupEphemeralKey');
|
||||||
await window.storage.put('isRestoredFromBackup', hasBackup);
|
await window.storage.put('isRestoredFromBackup', hasBackup);
|
||||||
|
|
||||||
log.info(`backups.download: done, had backup=${hasBackup}`);
|
log.info(`backups.downloadAndImport: done, had backup=${hasBackup}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public retryDownload(): void {
|
public retryDownload(): void {
|
||||||
|
@ -454,7 +502,7 @@ export class BackupsService {
|
||||||
return { isInBackupTier: true, cdnNumber: storedInfo.cdnNumber };
|
return { isInBackupTier: true, cdnNumber: storedInfo.cdnNumber };
|
||||||
}
|
}
|
||||||
|
|
||||||
private async doDownload({
|
private async doDownloadAndImport({
|
||||||
downloadPath,
|
downloadPath,
|
||||||
ephemeralKey,
|
ephemeralKey,
|
||||||
onProgress,
|
onProgress,
|
||||||
|
@ -509,7 +557,17 @@ export class BackupsService {
|
||||||
if (controller.signal.aborted) {
|
if (controller.signal.aborted) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
throw error;
|
|
||||||
|
// No backup on the server
|
||||||
|
if (error instanceof HTTPError && error.code === 404) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.error(
|
||||||
|
'backups.doDownloadAndImport: error downloading backup file',
|
||||||
|
Errors.toLogFormat(error)
|
||||||
|
);
|
||||||
|
throw new BackupDownloadFailedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (controller.signal.aborted) {
|
if (controller.signal.aborted) {
|
||||||
|
@ -551,6 +609,9 @@ export class BackupsService {
|
||||||
|
|
||||||
// Restore password on success
|
// Restore password on success
|
||||||
await window.storage.put('password', password);
|
await window.storage.put('password', password);
|
||||||
|
} catch (e) {
|
||||||
|
// Error during import; this is non-retriable
|
||||||
|
throw new BackupProcessingError();
|
||||||
} finally {
|
} finally {
|
||||||
await unlink(downloadPath);
|
await unlink(downloadPath);
|
||||||
}
|
}
|
||||||
|
@ -560,11 +621,6 @@ export class BackupsService {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No backup on the server
|
|
||||||
if (error instanceof HTTPError && error.code === 404) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Other errors bubble up and can be retried
|
// Other errors bubble up and can be retried
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,7 +116,7 @@ export const SmartInstallScreen = memo(function SmartInstallScreen() {
|
||||||
error: installerState.error,
|
error: installerState.error,
|
||||||
onCancel: onCancelBackupImport,
|
onCancel: onCancelBackupImport,
|
||||||
onRetry: retryBackupImport,
|
onRetry: retryBackupImport,
|
||||||
|
onRestartLink: startInstaller,
|
||||||
updates,
|
updates,
|
||||||
currentVersion: window.getVersion(),
|
currentVersion: window.getVersion(),
|
||||||
forceUpdate,
|
forceUpdate,
|
||||||
|
|
|
@ -18,8 +18,9 @@ export enum InstallScreenBackupStep {
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum InstallScreenBackupError {
|
export enum InstallScreenBackupError {
|
||||||
Unknown = 'Unknown',
|
|
||||||
UnsupportedVersion = 'UnsupportedVersion',
|
UnsupportedVersion = 'UnsupportedVersion',
|
||||||
|
Retriable = 'Retriable',
|
||||||
|
Fatal = 'Fatal',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum InstallScreenError {
|
export enum InstallScreenError {
|
||||||
|
|
Loading…
Add table
Reference in a new issue