Handle backup transfer errors during linking

This commit is contained in:
automated-signal 2024-12-12 08:30:00 -06:00 committed by GitHub
parent 079590b4e5
commit dbe92c02f2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 92 additions and 32 deletions

View file

@ -1660,7 +1660,7 @@ export async function startApp(): Promise<void> {
}, },
}); });
log.info('afterStart: backup downloaded, resolving'); log.info('afterStart: backup download attempt completed, resolving');
backupReady.resolve(); backupReady.resolve();
} catch (error) { } catch (error) {
log.error('afterStart: backup download failed, rejecting'); log.error('afterStart: backup download failed, rejecting');

View file

@ -15,6 +15,8 @@ import type {
import type { BackupCredentials } from './credentials'; import type { BackupCredentials } from './credentials';
import { BackupCredentialType } from '../../types/backups'; import { BackupCredentialType } from '../../types/backups';
import { uploadFile } from '../../util/uploadAttachment'; import { uploadFile } from '../../util/uploadAttachment';
import { ContinueWithoutSyncingError, RelinkRequestedError } from './errors';
import { missingCaseError } from '../../util/missingCaseError';
export type DownloadOptionsType = Readonly<{ export type DownloadOptionsType = Readonly<{
downloadOffset: number; downloadOffset: number;
@ -109,10 +111,23 @@ export class BackupAPI {
onProgress, onProgress,
abortSignal, abortSignal,
}: DownloadOptionsType): Promise<Readable> { }: DownloadOptionsType): Promise<Readable> {
const { cdn, key } = await this.server.getTransferArchive({ const response = await this.server.getTransferArchive({
abortSignal, 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({ return this.server.getEphemeralBackupStream({
cdn, cdn,
key, key,

View file

@ -13,3 +13,7 @@ export class UnsupportedBackupVersion extends Error {
export class BackupDownloadFailedError extends Error {} export class BackupDownloadFailedError extends Error {}
export class BackupProcessingError extends Error {} export class BackupProcessingError extends Error {}
export class RelinkRequestedError extends Error {}
export class ContinueWithoutSyncingError extends Error {}

View file

@ -52,6 +52,8 @@ import { BackupType } from './types';
import { import {
BackupDownloadFailedError, BackupDownloadFailedError,
BackupProcessingError, BackupProcessingError,
ContinueWithoutSyncingError,
RelinkRequestedError,
UnsupportedBackupVersion, UnsupportedBackupVersion,
} from './errors'; } from './errors';
import { ToastType } from '../../types/Toast'; import { ToastType } from '../../types/Toast';
@ -149,7 +151,15 @@ export class BackupsService {
this.downloadRetryPromise = explodePromise<RetryBackupImportValue>(); this.downloadRetryPromise = explodePromise<RetryBackupImportValue>();
let installerError: InstallScreenBackupError; 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; installerError = InstallScreenBackupError.UnsupportedVersion;
log.error( log.error(
'backups.downloadAndImport: unsupported version', 'backups.downloadAndImport: unsupported version',
@ -167,30 +177,8 @@ export class BackupsService {
'backups.downloadAndImport: fatal error during processing; unlinking & deleting data', 'backups.downloadAndImport: fatal error during processing; unlinking & deleting data',
Errors.toLogFormat(error) Errors.toLogFormat(error)
); );
// eslint-disable-next-line no-await-in-loop
try { await this.unlinkAndDeleteAllData();
// 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 { } else {
log.error( log.error(
'backups.downloadAndImport: unknown error, prompting user to retry' 'backups.downloadAndImport: unknown error, prompting user to retry'
@ -222,6 +210,10 @@ 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);
if (!hasBackup) {
window.reduxActions.installer.handleMissingBackup();
}
log.info(`backups.downloadAndImport: done, had backup=${hasBackup}`); log.info(`backups.downloadAndImport: done, had backup=${hasBackup}`);
} }
@ -563,6 +555,19 @@ export class BackupsService {
return false; 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( log.error(
'backups.doDownloadAndImport: error downloading backup file', 'backups.doDownloadAndImport: error downloading backup file',
Errors.toLogFormat(error) 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 { public isImportRunning(): boolean {
return this.isRunning === 'import'; return this.isRunning === 'import';
} }

View file

@ -155,6 +155,7 @@ export const actions = {
updateBackupImportProgress, updateBackupImportProgress,
retryBackupImport, retryBackupImport,
showBackupImport, showBackupImport,
handleMissingBackup,
showLinkInProgress, showLinkInProgress,
}; };
@ -450,6 +451,11 @@ function showLinkInProgress(): ShowLinkInProgressActionType {
return { type: SHOW_LINK_IN_PROGRESS }; 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( function updateBackupImportProgress(
payload: UpdateBackupImportProgressActionType['payload'] payload: UpdateBackupImportProgressActionType['payload']
): UpdateBackupImportProgressActionType { ): UpdateBackupImportProgressActionType {

View file

@ -1289,10 +1289,18 @@ const StickerPackUploadFormSchema = z.object({
stickers: z.array(StickerPackUploadAttributesSchema), stickers: z.array(StickerPackUploadAttributesSchema),
}); });
const TransferArchiveSchema = z.object({ const TransferArchiveSchema = z.union([
cdn: z.number(), z.object({
key: z.string(), 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<typeof TransferArchiveSchema>; export type TransferArchiveType = z.infer<typeof TransferArchiveSchema>;
@ -2381,7 +2389,7 @@ export function initialize({
}); });
if (response.status === 200) { if (response.status === 200) {
return TransferArchiveSchema.parse(data); return parseUnknown(TransferArchiveSchema, data);
} }
strictAssert( strictAssert(