diff --git a/_locales/en/messages.json b/_locales/en/messages.json index ac7786b112..8a2c478870 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -4791,6 +4791,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" diff --git a/ts/background.ts b/ts/background.ts index 2a67b9d619..9cffed6f50 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -1638,24 +1638,29 @@ export async function startApp(): Promise { 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); diff --git a/ts/components/ConfirmationDialog.tsx b/ts/components/ConfirmationDialog.tsx index f8c760942b..bb32d27686 100644 --- a/ts/components/ConfirmationDialog.tsx +++ b/ts/components/ConfirmationDialog.tsx @@ -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({ 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 = ( ); } + } else if (error === InstallScreenBackupError.Fatal) { + errorElem = ( + null} + noMouseClose + noDefaultCancelButton + noEscapeClose + > + {i18n('icu:BackupImportScreen__error-fatal__body')} + + ); } else { throw missingCaseError(error); } diff --git a/ts/services/backups/errors.ts b/ts/services/backups/errors.ts index 9b48b7b147..b3e9211af1 100644 --- a/ts/services/backups/errors.ts +++ b/ts/services/backups/errors.ts @@ -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 {} diff --git a/ts/services/backups/index.ts b/ts/services/backups/index.ts index 09be2954c8..a07a9d0378 100644 --- a/ts/services/backups/index.ts +++ b/ts/services/backups/index.ts @@ -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 { + public async downloadAndImport(options: DownloadOptionsType): Promise { 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(); + + 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; } diff --git a/ts/state/smart/InstallScreen.tsx b/ts/state/smart/InstallScreen.tsx index b4e7544797..287bc7e947 100644 --- a/ts/state/smart/InstallScreen.tsx +++ b/ts/state/smart/InstallScreen.tsx @@ -116,7 +116,7 @@ export const SmartInstallScreen = memo(function SmartInstallScreen() { error: installerState.error, onCancel: onCancelBackupImport, onRetry: retryBackupImport, - + onRestartLink: startInstaller, updates, currentVersion: window.getVersion(), forceUpdate, diff --git a/ts/types/InstallScreen.ts b/ts/types/InstallScreen.ts index 3faa33fbc9..f71e9b06d2 100644 --- a/ts/types/InstallScreen.ts +++ b/ts/types/InstallScreen.ts @@ -18,8 +18,9 @@ export enum InstallScreenBackupStep { } export enum InstallScreenBackupError { - Unknown = 'Unknown', UnsupportedVersion = 'UnsupportedVersion', + Retriable = 'Retriable', + Fatal = 'Fatal', } export enum InstallScreenError {