Handle fatal error during backup import
This commit is contained in:
parent
b5c5cd97af
commit
079590b4e5
9 changed files with 146 additions and 41 deletions
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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({});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -116,7 +116,7 @@ export const SmartInstallScreen = memo(function SmartInstallScreen() {
|
|||
error: installerState.error,
|
||||
onCancel: onCancelBackupImport,
|
||||
onRetry: retryBackupImport,
|
||||
|
||||
onRestartLink: startInstaller,
|
||||
updates,
|
||||
currentVersion: window.getVersion(),
|
||||
forceUpdate,
|
||||
|
|
|
@ -18,8 +18,9 @@ export enum InstallScreenBackupStep {
|
|||
}
|
||||
|
||||
export enum InstallScreenBackupError {
|
||||
Unknown = 'Unknown',
|
||||
UnsupportedVersion = 'UnsupportedVersion',
|
||||
Retriable = 'Retriable',
|
||||
Fatal = 'Fatal',
|
||||
}
|
||||
|
||||
export enum InstallScreenError {
|
||||
|
|
Loading…
Reference in a new issue