Handle backup transfer errors during linking
This commit is contained in:
parent
079590b4e5
commit
dbe92c02f2
6 changed files with 92 additions and 32 deletions
|
@ -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');
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in a new issue