Handle fatal error during backup import

This commit is contained in:
trevor-signal 2024-12-05 11:35:37 -05:00 committed by GitHub
parent 94dba11bcb
commit 10eeb63776
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 146 additions and 41 deletions

View file

@ -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"

View file

@ -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);

View file

@ -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}

View file

@ -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({});

View file

@ -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);
} }

View file

@ -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 {}

View file

@ -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;
} }

View file

@ -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,

View file

@ -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 {