Handle fatal error during backup import

This commit is contained in:
automated-signal 2024-12-12 08:15:40 -06:00 committed by GitHub
parent b5c5cd97af
commit 079590b4e5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 146 additions and 41 deletions

View file

@ -4815,6 +4815,10 @@
"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"
},
"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": {
"messageformat": "Retry",
"description": "Text of the retry button of the error modal in the backup import screen"

View file

@ -1646,24 +1646,29 @@ export async function startApp(): Promise<void> {
onOffline();
}
// Download backup before enabling request handler and storage service
try {
await backupsService.download({
onProgress: (backupStep, currentBytes, totalBytes) => {
window.reduxActions.installer.updateBackupImportProgress({
backupStep,
currentBytes,
totalBytes,
});
},
});
const backupDownloadPath = window.storage.get('backupDownloadPath');
if (backupDownloadPath) {
// Download backup before enabling request handler and storage service
try {
await backupsService.downloadAndImport({
onProgress: (backupStep, currentBytes, totalBytes) => {
window.reduxActions.installer.updateBackupImportProgress({
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();
} catch (error) {
log.error('afterStart: backup download failed, rejecting');
backupReady.reject(error);
throw error;
}
server.registerRequestHandler(messageReceiver);

View file

@ -39,6 +39,7 @@ export type OwnProps = Readonly<{
hasXButton?: boolean;
i18n: LocalizerType;
moduleClassName?: string;
noEscapeClose?: boolean;
noMouseClose?: boolean;
noDefaultCancelButton?: boolean;
onCancel?: () => unknown;
@ -80,6 +81,7 @@ export const ConfirmationDialog = React.memo(function ConfirmationDialogInner({
i18n,
isSpinning,
moduleClassName,
noEscapeClose,
noMouseClose,
noDefaultCancelButton,
onCancel,
@ -163,6 +165,7 @@ export const ConfirmationDialog = React.memo(function ConfirmationDialogInner({
<ModalHost
modalName={modalName}
noMouseClose={noMouseClose}
noEscapeClose={noEscapeClose}
onClose={close}
onEscape={cancelAndClose}
onTopOfEverything={onTopOfEverything}

View file

@ -99,7 +99,15 @@ Error.args = {
backupStep: InstallScreenBackupStep.Download,
currentBytes: 500 * 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({});

View file

@ -32,6 +32,7 @@ export type PropsType = Readonly<{
error?: InstallScreenBackupError;
onCancel: () => void;
onRetry: () => void;
onRestartLink: () => void;
// Updater UI
updates: UpdatesStateType;
@ -49,6 +50,7 @@ export function InstallScreenBackupImportStep({
error,
onCancel,
onRetry,
onRestartLink,
updates,
currentVersion,
@ -158,7 +160,7 @@ export function InstallScreenBackupImportStep({
OS={OS}
/>
);
} else if (error === InstallScreenBackupError.Unknown) {
} else if (error === InstallScreenBackupError.Retriable) {
if (!isConfirmingSkip) {
errorElem = (
<ConfirmationDialog
@ -179,6 +181,27 @@ export function InstallScreenBackupImportStep({
</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 {
throw missingCaseError(error);
}

View file

@ -1,5 +1,6 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable max-classes-per-file */
import type Long from 'long';
@ -8,3 +9,7 @@ export class UnsupportedBackupVersion extends Error {
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 { validateBackup } from './validator';
import { BackupType } from './types';
import { UnsupportedBackupVersion } from './errors';
import {
BackupDownloadFailedError,
BackupProcessingError,
UnsupportedBackupVersion,
} from './errors';
import { ToastType } from '../../types/Toast';
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');
if (!backupDownloadPath) {
log.warn('backups.download: no backup download path, skipping');
log.warn('backups.downloadAndImport: no backup download path, skipping');
return;
}
log.info('backups.download: downloading...');
log.info('backups.downloadAndImport: downloading...');
const ephemeralKey = window.storage.get('backupEphemeralKey');
@ -136,22 +140,66 @@ export class BackupsService {
while (true) {
try {
// eslint-disable-next-line no-await-in-loop
hasBackup = await this.doDownload({
hasBackup = await this.doDownloadAndImport({
downloadPath: absoluteDownloadPath,
onProgress: options.onProgress,
ephemeralKey,
});
} catch (error) {
log.warn(
'backups.download: error, prompting user to retry',
Errors.toLogFormat(error)
);
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({
error:
error instanceof UnsupportedBackupVersion
? InstallScreenBackupError.UnsupportedVersion
: InstallScreenBackupError.Unknown,
error: installerError,
});
// eslint-disable-next-line no-await-in-loop
@ -174,7 +222,7 @@ export class BackupsService {
await window.storage.remove('backupEphemeralKey');
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 {
@ -454,7 +502,7 @@ export class BackupsService {
return { isInBackupTier: true, cdnNumber: storedInfo.cdnNumber };
}
private async doDownload({
private async doDownloadAndImport({
downloadPath,
ephemeralKey,
onProgress,
@ -509,7 +557,17 @@ export class BackupsService {
if (controller.signal.aborted) {
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) {
@ -551,6 +609,9 @@ export class BackupsService {
// Restore password on success
await window.storage.put('password', password);
} catch (e) {
// Error during import; this is non-retriable
throw new BackupProcessingError();
} finally {
await unlink(downloadPath);
}
@ -560,11 +621,6 @@ export class BackupsService {
return false;
}
// No backup on the server
if (error instanceof HTTPError && error.code === 404) {
return false;
}
// Other errors bubble up and can be retried
throw error;
}

View file

@ -116,7 +116,7 @@ export const SmartInstallScreen = memo(function SmartInstallScreen() {
error: installerState.error,
onCancel: onCancelBackupImport,
onRetry: retryBackupImport,
onRestartLink: startInstaller,
updates,
currentVersion: window.getVersion(),
forceUpdate,

View file

@ -18,8 +18,9 @@ export enum InstallScreenBackupStep {
}
export enum InstallScreenBackupError {
Unknown = 'Unknown',
UnsupportedVersion = 'UnsupportedVersion',
Retriable = 'Retriable',
Fatal = 'Fatal',
}
export enum InstallScreenError {