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();
} catch (error) {
log.error('afterStart: backup download failed, rejecting');

View file

@ -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<Readable> {
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,

View file

@ -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 {}

View file

@ -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<RetryBackupImportValue>();
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';
}

View file

@ -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 {

View file

@ -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<typeof TransferArchiveSchema>;
@ -2381,7 +2389,7 @@ export function initialize({
});
if (response.status === 200) {
return TransferArchiveSchema.parse(data);
return parseUnknown(TransferArchiveSchema, data);
}
strictAssert(