diff --git a/ts/background.ts b/ts/background.ts index 0ffc5f29e..423b63106 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -1660,7 +1660,7 @@ export async function startApp(): Promise { }, }); - log.info('afterStart: backup downloaded, resolving'); + log.info('afterStart: backup download attempt completed, resolving'); backupReady.resolve(); } catch (error) { log.error('afterStart: backup download failed, rejecting'); diff --git a/ts/services/backups/api.ts b/ts/services/backups/api.ts index b54c3b515..0afb1eca1 100644 --- a/ts/services/backups/api.ts +++ b/ts/services/backups/api.ts @@ -15,6 +15,8 @@ import type { import type { BackupCredentials } from './credentials'; import { BackupCredentialType } from '../../types/backups'; import { uploadFile } from '../../util/uploadAttachment'; +import { ContinueWithoutSyncingError, RelinkRequestedError } from './errors'; +import { missingCaseError } from '../../util/missingCaseError'; export type DownloadOptionsType = Readonly<{ downloadOffset: number; @@ -109,10 +111,23 @@ export class BackupAPI { onProgress, abortSignal, }: DownloadOptionsType): Promise { - const { cdn, key } = await this.server.getTransferArchive({ + const response = await this.server.getTransferArchive({ abortSignal, }); + if ('error' in response) { + switch (response.error) { + case 'RELINK_REQUESTED': + throw new RelinkRequestedError(); + case 'CONTINUE_WITHOUT_UPLOAD': + throw new ContinueWithoutSyncingError(); + default: + throw missingCaseError(response.error); + } + } + + const { cdn, key } = response; + return this.server.getEphemeralBackupStream({ cdn, key, diff --git a/ts/services/backups/errors.ts b/ts/services/backups/errors.ts index b3e9211af..55b76755c 100644 --- a/ts/services/backups/errors.ts +++ b/ts/services/backups/errors.ts @@ -13,3 +13,7 @@ export class UnsupportedBackupVersion extends Error { export class BackupDownloadFailedError extends Error {} export class BackupProcessingError extends Error {} + +export class RelinkRequestedError extends Error {} + +export class ContinueWithoutSyncingError extends Error {} diff --git a/ts/services/backups/index.ts b/ts/services/backups/index.ts index 02bb54489..97416865c 100644 --- a/ts/services/backups/index.ts +++ b/ts/services/backups/index.ts @@ -52,6 +52,8 @@ import { BackupType } from './types'; import { BackupDownloadFailedError, BackupProcessingError, + ContinueWithoutSyncingError, + RelinkRequestedError, UnsupportedBackupVersion, } from './errors'; import { ToastType } from '../../types/Toast'; @@ -149,7 +151,15 @@ export class BackupsService { this.downloadRetryPromise = explodePromise(); let installerError: InstallScreenBackupError; - if (error instanceof UnsupportedBackupVersion) { + if (error instanceof RelinkRequestedError) { + installerError = InstallScreenBackupError.Fatal; + log.error( + 'backups.downloadAndImport: primary requested relink; unlinking & deleting data', + Errors.toLogFormat(error) + ); + // eslint-disable-next-line no-await-in-loop + await this.unlinkAndDeleteAllData(); + } else if (error instanceof UnsupportedBackupVersion) { installerError = InstallScreenBackupError.UnsupportedVersion; log.error( 'backups.downloadAndImport: unsupported version', @@ -167,30 +177,8 @@ export class BackupsService { '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) - ); - } + // eslint-disable-next-line no-await-in-loop + await this.unlinkAndDeleteAllData(); } else { log.error( 'backups.downloadAndImport: unknown error, prompting user to retry' @@ -222,6 +210,10 @@ export class BackupsService { await window.storage.remove('backupEphemeralKey'); await window.storage.put('isRestoredFromBackup', hasBackup); + if (!hasBackup) { + window.reduxActions.installer.handleMissingBackup(); + } + log.info(`backups.downloadAndImport: done, had backup=${hasBackup}`); } @@ -563,6 +555,19 @@ export class BackupsService { return false; } + // Primary decided to abort syncing process; continue on with no backup + if (error instanceof ContinueWithoutSyncingError) { + log.error( + 'backups.doDownloadAndImport: primary requested to continue without syncing' + ); + return false; + } + + // Primary wants to try link & sync again + if (error instanceof RelinkRequestedError) { + throw error; + } + log.error( 'backups.doDownloadAndImport: error downloading backup file', Errors.toLogFormat(error) @@ -702,6 +707,28 @@ export class BackupsService { } } + private async unlinkAndDeleteAllData() { + try { + 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.unlinkAndDeleteAllData: deleting all data'); + await window.textsecure.storage.protocol.removeAllData(); + log.info('backups.unlinkAndDeleteAllData: all data deleted successfully'); + } catch (e) { + log.error( + 'backups.unlinkAndDeleteAllData: unable to remove all data', + Errors.toLogFormat(e) + ); + } + } + public isImportRunning(): boolean { return this.isRunning === 'import'; } diff --git a/ts/state/ducks/installer.ts b/ts/state/ducks/installer.ts index 1341ca1f9..97408d840 100644 --- a/ts/state/ducks/installer.ts +++ b/ts/state/ducks/installer.ts @@ -155,6 +155,7 @@ export const actions = { updateBackupImportProgress, retryBackupImport, showBackupImport, + handleMissingBackup, showLinkInProgress, }; @@ -450,6 +451,11 @@ function showLinkInProgress(): ShowLinkInProgressActionType { return { type: SHOW_LINK_IN_PROGRESS }; } +function handleMissingBackup(): ShowLinkInProgressActionType { + // If backup is missing, go to normal link-in-progress view + return { type: SHOW_LINK_IN_PROGRESS }; +} + function updateBackupImportProgress( payload: UpdateBackupImportProgressActionType['payload'] ): UpdateBackupImportProgressActionType { diff --git a/ts/textsecure/WebAPI.ts b/ts/textsecure/WebAPI.ts index e77ec6652..6e856da50 100644 --- a/ts/textsecure/WebAPI.ts +++ b/ts/textsecure/WebAPI.ts @@ -1289,10 +1289,18 @@ const StickerPackUploadFormSchema = z.object({ stickers: z.array(StickerPackUploadAttributesSchema), }); -const TransferArchiveSchema = z.object({ - cdn: z.number(), - key: z.string(), -}); +const TransferArchiveSchema = z.union([ + z.object({ + cdn: z.number(), + key: z.string(), + }), + z.object({ + error: z.union([ + z.literal('RELINK_REQUESTED'), + z.literal('CONTINUE_WITHOUT_UPLOAD'), + ]), + }), +]); export type TransferArchiveType = z.infer; @@ -2381,7 +2389,7 @@ export function initialize({ }); if (response.status === 200) { - return TransferArchiveSchema.parse(data); + return parseUnknown(TransferArchiveSchema, data); } strictAssert(