Allow cancel during backup import processing
This commit is contained in:
parent
721a875c44
commit
8fef392e7f
7 changed files with 143 additions and 83 deletions
|
@ -4803,6 +4803,10 @@
|
||||||
"messageformat": "Finalizing message transfer...",
|
"messageformat": "Finalizing message transfer...",
|
||||||
"description": "Hint under the progressbar in the backup import screen shown after the backup file is downloaded and while backup data is being processed"
|
"description": "Hint under the progressbar in the backup import screen shown after the backup file is downloaded and while backup data is being processed"
|
||||||
},
|
},
|
||||||
|
"icu:BackupImportScreen__progressbar-hint--canceling": {
|
||||||
|
"messageformat": "Canceling...",
|
||||||
|
"description": "Hint under the progressbar in the backup import screen after user cancels"
|
||||||
|
},
|
||||||
"icu:BackupImportScreen__progressbar-hint--preparing": {
|
"icu:BackupImportScreen__progressbar-hint--preparing": {
|
||||||
"messageformat": "Preparing to download...",
|
"messageformat": "Preparing to download...",
|
||||||
"description": "Hint under the progressbar in the backup import screen when download size is not yet known"
|
"description": "Hint under the progressbar in the backup import screen when download size is not yet known"
|
||||||
|
@ -4820,19 +4824,19 @@
|
||||||
"description": "Text shown while importing messages & chats from the user's primary device."
|
"description": "Text shown while importing messages & chats from the user's primary device."
|
||||||
},
|
},
|
||||||
"icu:BackupImportScreen__cancel-confirmation__title": {
|
"icu:BackupImportScreen__cancel-confirmation__title": {
|
||||||
"messageformat": "Cancel transfer?",
|
"messageformat": "Cancel device linking?",
|
||||||
"description": "Title of the cancel confirmation modal in the backup import screen"
|
"description": "Title of the cancel confirmation modal in the backup import screen"
|
||||||
},
|
},
|
||||||
"icu:BackupImportScreen__cancel-confirmation__body": {
|
"icu:BackupImportScreen__cancel-confirmation__body": {
|
||||||
"messageformat": "Your messages and media have not completed restoring. If you choose to cancel, your device will be linked without your message history.",
|
"messageformat": "If you choose to cancel, this device will not be linked and no messages or media will be transferred.",
|
||||||
"description": "Body of the cancel confirmation modal in the backup import screen"
|
"description": "Body of the cancel confirmation modal in the backup import screen"
|
||||||
},
|
},
|
||||||
"icu:BackupImportScreen__cancel-confirmation__cancel": {
|
"icu:BackupImportScreen__cancel-confirmation__cancel": {
|
||||||
"messageformat": "Continue transfer",
|
"messageformat": "Continue linking",
|
||||||
"description": "Text of the continue button of the cancel confirmation modal in the backup import screen"
|
"description": "Text of the continue button of the cancel confirmation modal in the backup import screen"
|
||||||
},
|
},
|
||||||
"icu:BackupImportScreen__cancel-confirmation__confirm": {
|
"icu:BackupImportScreen__cancel-confirmation__confirm": {
|
||||||
"messageformat": "Cancel transfer",
|
"messageformat": "Cancel linking",
|
||||||
"description": "Text of the confirmation button of the cancel confirmation modal in the backup import screen"
|
"description": "Text of the confirmation button of the cancel confirmation modal in the backup import screen"
|
||||||
},
|
},
|
||||||
"icu:BackupImportScreen__error__title": {
|
"icu:BackupImportScreen__error__title": {
|
||||||
|
|
|
@ -147,6 +147,14 @@ FatalError.args = {
|
||||||
error: InstallScreenBackupError.Fatal,
|
error: InstallScreenBackupError.Fatal,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const Canceled = Template.bind({});
|
||||||
|
Canceled.args = {
|
||||||
|
backupStep: InstallScreenBackupStep.Process,
|
||||||
|
currentBytes: 500 * 1024,
|
||||||
|
totalBytes: 1024 * 1024,
|
||||||
|
error: InstallScreenBackupError.Canceled,
|
||||||
|
};
|
||||||
|
|
||||||
export const UnsupportedVersion = Template.bind({});
|
export const UnsupportedVersion = Template.bind({});
|
||||||
UnsupportedVersion.args = {
|
UnsupportedVersion.args = {
|
||||||
backupStep: InstallScreenBackupStep.Process,
|
backupStep: InstallScreenBackupStep.Process,
|
||||||
|
|
|
@ -69,7 +69,6 @@ export function InstallScreenBackupImportStep(props: PropsType): JSX.Element {
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [isConfirmingCancel, setIsConfirmingCancel] = useState(false);
|
const [isConfirmingCancel, setIsConfirmingCancel] = useState(false);
|
||||||
const [isConfirmingSkip, setIsConfirmingSkip] = useState(false);
|
|
||||||
|
|
||||||
const confirmCancel = useCallback(() => {
|
const confirmCancel = useCallback(() => {
|
||||||
setIsConfirmingCancel(true);
|
setIsConfirmingCancel(true);
|
||||||
|
@ -84,22 +83,9 @@ export function InstallScreenBackupImportStep(props: PropsType): JSX.Element {
|
||||||
setIsConfirmingCancel(false);
|
setIsConfirmingCancel(false);
|
||||||
}, [onCancel]);
|
}, [onCancel]);
|
||||||
|
|
||||||
const confirmSkip = useCallback(() => {
|
|
||||||
setIsConfirmingSkip(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const abortSkip = useCallback(() => {
|
|
||||||
setIsConfirmingSkip(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onSkipWrap = useCallback(() => {
|
|
||||||
onCancel();
|
|
||||||
setIsConfirmingSkip(false);
|
|
||||||
}, [onCancel]);
|
|
||||||
|
|
||||||
const onRetryWrap = useCallback(() => {
|
const onRetryWrap = useCallback(() => {
|
||||||
onRetry();
|
onRetry();
|
||||||
setIsConfirmingSkip(false);
|
setIsConfirmingCancel(false);
|
||||||
}, [onRetry]);
|
}, [onRetry]);
|
||||||
|
|
||||||
const learnMoreLink = (parts: Array<string | JSX.Element>) => (
|
const learnMoreLink = (parts: Array<string | JSX.Element>) => (
|
||||||
|
@ -109,7 +95,7 @@ export function InstallScreenBackupImportStep(props: PropsType): JSX.Element {
|
||||||
);
|
);
|
||||||
|
|
||||||
let errorElem: JSX.Element | undefined;
|
let errorElem: JSX.Element | undefined;
|
||||||
if (error == null) {
|
if (error == null || error === InstallScreenBackupError.Canceled) {
|
||||||
// no-op
|
// no-op
|
||||||
} else if (error === InstallScreenBackupError.UnsupportedVersion) {
|
} else if (error === InstallScreenBackupError.UnsupportedVersion) {
|
||||||
errorElem = (
|
errorElem = (
|
||||||
|
@ -120,17 +106,19 @@ export function InstallScreenBackupImportStep(props: PropsType): JSX.Element {
|
||||||
startUpdate={startUpdate}
|
startUpdate={startUpdate}
|
||||||
forceUpdate={forceUpdate}
|
forceUpdate={forceUpdate}
|
||||||
currentVersion={currentVersion}
|
currentVersion={currentVersion}
|
||||||
onClose={confirmSkip}
|
onClose={confirmCancel}
|
||||||
OS={OS}
|
OS={OS}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (error === InstallScreenBackupError.Retriable) {
|
} else if (error === InstallScreenBackupError.Retriable) {
|
||||||
if (!isConfirmingSkip) {
|
if (!isConfirmingCancel) {
|
||||||
errorElem = (
|
errorElem = (
|
||||||
<ConfirmationDialog
|
<ConfirmationDialog
|
||||||
dialogName="InstallScreenBackupImportStep.error"
|
dialogName="InstallScreenBackupImportStep.error"
|
||||||
title={i18n('icu:BackupImportScreen__error__title')}
|
title={i18n('icu:BackupImportScreen__error__title')}
|
||||||
cancelText={i18n('icu:BackupImportScreen__skip')}
|
cancelText={i18n(
|
||||||
|
'icu:BackupImportScreen__cancel-confirmation__confirm'
|
||||||
|
)}
|
||||||
actions={[
|
actions={[
|
||||||
{
|
{
|
||||||
action: onRetryWrap,
|
action: onRetryWrap,
|
||||||
|
@ -139,7 +127,7 @@ export function InstallScreenBackupImportStep(props: PropsType): JSX.Element {
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
onClose={confirmSkip}
|
onClose={confirmCancel}
|
||||||
>
|
>
|
||||||
{i18n('icu:BackupImportScreen__error__body')}
|
{i18n('icu:BackupImportScreen__error__body')}
|
||||||
</ConfirmationDialog>
|
</ConfirmationDialog>
|
||||||
|
@ -170,6 +158,24 @@ export function InstallScreenBackupImportStep(props: PropsType): JSX.Element {
|
||||||
throw missingCaseError(error);
|
throw missingCaseError(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isCanceled = error === InstallScreenBackupError.Canceled;
|
||||||
|
let cancelButton: JSX.Element | undefined;
|
||||||
|
if (
|
||||||
|
!isCanceled &&
|
||||||
|
(backupStep === InstallScreenBackupStep.Download ||
|
||||||
|
backupStep === InstallScreenBackupStep.Process)
|
||||||
|
) {
|
||||||
|
cancelButton = (
|
||||||
|
<button
|
||||||
|
className="InstallScreenBackupImportStep__cancel"
|
||||||
|
type="button"
|
||||||
|
onClick={confirmCancel}
|
||||||
|
>
|
||||||
|
{i18n('icu:BackupImportScreen__cancel')}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="InstallScreenBackupImportStep">
|
<div className="InstallScreenBackupImportStep">
|
||||||
<TitlebarDragArea />
|
<TitlebarDragArea />
|
||||||
|
@ -178,10 +184,12 @@ export function InstallScreenBackupImportStep(props: PropsType): JSX.Element {
|
||||||
<h3 className="InstallScreenBackupImportStep__title">
|
<h3 className="InstallScreenBackupImportStep__title">
|
||||||
{i18n('icu:BackupImportScreen__title')}
|
{i18n('icu:BackupImportScreen__title')}
|
||||||
</h3>
|
</h3>
|
||||||
<ProgressBarAndDescription {...props} />
|
<ProgressBarAndDescription {...props} isCanceled={isCanceled} />
|
||||||
<div className="InstallScreenBackupImportStep__description">
|
{!isCanceled && (
|
||||||
{i18n('icu:BackupImportScreen__description')}
|
<div className="InstallScreenBackupImportStep__description">
|
||||||
</div>
|
{i18n('icu:BackupImportScreen__description')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="InstallScreenBackupImportStep__footer">
|
<div className="InstallScreenBackupImportStep__footer">
|
||||||
<div className="InstallScreenBackupImportStep__security">
|
<div className="InstallScreenBackupImportStep__security">
|
||||||
|
@ -195,15 +203,7 @@ export function InstallScreenBackupImportStep(props: PropsType): JSX.Element {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{backupStep === InstallScreenBackupStep.Download && (
|
{cancelButton}
|
||||||
<button
|
|
||||||
className="InstallScreenBackupImportStep__cancel"
|
|
||||||
type="button"
|
|
||||||
onClick={confirmCancel}
|
|
||||||
>
|
|
||||||
{i18n('icu:BackupImportScreen__cancel')}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isConfirmingCancel && (
|
{isConfirmingCancel && (
|
||||||
|
@ -229,25 +229,6 @@ export function InstallScreenBackupImportStep(props: PropsType): JSX.Element {
|
||||||
</ConfirmationDialog>
|
</ConfirmationDialog>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isConfirmingSkip && (
|
|
||||||
<ConfirmationDialog
|
|
||||||
dialogName="InstallScreenBackupImportStep.confirmSkip"
|
|
||||||
title={i18n('icu:BackupImportScreen__skip-confirmation__title')}
|
|
||||||
cancelText={i18n('icu:BackupImportScreen__skip-confirmation__cancel')}
|
|
||||||
actions={[
|
|
||||||
{
|
|
||||||
action: onSkipWrap,
|
|
||||||
style: 'affirmative',
|
|
||||||
text: i18n('icu:BackupImportScreen__skip'),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
i18n={i18n}
|
|
||||||
onClose={abortSkip}
|
|
||||||
>
|
|
||||||
{i18n('icu:BackupImportScreen__skip-confirmation__body')}
|
|
||||||
</ConfirmationDialog>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{errorElem}
|
{errorElem}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -256,6 +237,7 @@ export function InstallScreenBackupImportStep(props: PropsType): JSX.Element {
|
||||||
type ProgressBarPropsType = Readonly<
|
type ProgressBarPropsType = Readonly<
|
||||||
{
|
{
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
isCanceled: boolean;
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
backupStep: InstallScreenBackupStep.WaitForBackup;
|
backupStep: InstallScreenBackupStep.WaitForBackup;
|
||||||
|
@ -271,7 +253,7 @@ type ProgressBarPropsType = Readonly<
|
||||||
>;
|
>;
|
||||||
|
|
||||||
function ProgressBarAndDescription(props: ProgressBarPropsType): JSX.Element {
|
function ProgressBarAndDescription(props: ProgressBarPropsType): JSX.Element {
|
||||||
const { backupStep, i18n } = props;
|
const { backupStep, i18n, isCanceled } = props;
|
||||||
if (backupStep === InstallScreenBackupStep.WaitForBackup) {
|
if (backupStep === InstallScreenBackupStep.WaitForBackup) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -292,6 +274,20 @@ function ProgressBarAndDescription(props: ProgressBarPropsType): JSX.Element {
|
||||||
currentBytes / totalBytes
|
currentBytes / totalBytes
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isCanceled) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ProgressBar
|
||||||
|
fractionComplete={fractionComplete}
|
||||||
|
isRTL={i18n.getLocaleDirection() === 'rtl'}
|
||||||
|
/>
|
||||||
|
<div className="InstallScreenBackupImportStep__progressbar-hint">
|
||||||
|
{i18n('icu:BackupImportScreen__progressbar-hint--canceling')}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (backupStep === InstallScreenBackupStep.Download) {
|
if (backupStep === InstallScreenBackupStep.Download) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -14,6 +14,8 @@ export class BackupDownloadFailedError extends Error {}
|
||||||
|
|
||||||
export class BackupProcessingError extends Error {}
|
export class BackupProcessingError extends Error {}
|
||||||
|
|
||||||
|
export class BackupImportCanceledError extends Error {}
|
||||||
|
|
||||||
export class RelinkRequestedError extends Error {}
|
export class RelinkRequestedError extends Error {}
|
||||||
|
|
||||||
export class ContinueWithoutSyncingError extends Error {}
|
export class ContinueWithoutSyncingError extends Error {}
|
||||||
|
|
|
@ -51,6 +51,7 @@ import { validateBackup } from './validator';
|
||||||
import { BackupType } from './types';
|
import { BackupType } from './types';
|
||||||
import {
|
import {
|
||||||
BackupDownloadFailedError,
|
BackupDownloadFailedError,
|
||||||
|
BackupImportCanceledError,
|
||||||
BackupProcessingError,
|
BackupProcessingError,
|
||||||
ContinueWithoutSyncingError,
|
ContinueWithoutSyncingError,
|
||||||
RelinkRequestedError,
|
RelinkRequestedError,
|
||||||
|
@ -93,6 +94,7 @@ export type ImportOptionsType = Readonly<{
|
||||||
export class BackupsService {
|
export class BackupsService {
|
||||||
#isStarted = false;
|
#isStarted = false;
|
||||||
#isRunning: 'import' | 'export' | false = false;
|
#isRunning: 'import' | 'export' | false = false;
|
||||||
|
#importController: AbortController | undefined;
|
||||||
#downloadController: AbortController | undefined;
|
#downloadController: AbortController | undefined;
|
||||||
|
|
||||||
#downloadRetryPromise:
|
#downloadRetryPromise:
|
||||||
|
@ -152,14 +154,14 @@ export class BackupsService {
|
||||||
this.#downloadRetryPromise = explodePromise<RetryBackupImportValue>();
|
this.#downloadRetryPromise = explodePromise<RetryBackupImportValue>();
|
||||||
|
|
||||||
let installerError: InstallScreenBackupError;
|
let installerError: InstallScreenBackupError;
|
||||||
|
let shouldUnlinkAndDeleteData = false;
|
||||||
if (error instanceof RelinkRequestedError) {
|
if (error instanceof RelinkRequestedError) {
|
||||||
installerError = InstallScreenBackupError.Fatal;
|
installerError = InstallScreenBackupError.Fatal;
|
||||||
log.error(
|
log.error(
|
||||||
'backups.downloadAndImport: primary requested relink; unlinking & deleting data',
|
'backups.downloadAndImport: primary requested relink; unlinking & deleting data',
|
||||||
Errors.toLogFormat(error)
|
Errors.toLogFormat(error)
|
||||||
);
|
);
|
||||||
// eslint-disable-next-line no-await-in-loop
|
shouldUnlinkAndDeleteData = true;
|
||||||
await this.#unlinkAndDeleteAllData();
|
|
||||||
} else if (error instanceof UnsupportedBackupVersion) {
|
} else if (error instanceof UnsupportedBackupVersion) {
|
||||||
installerError = InstallScreenBackupError.UnsupportedVersion;
|
installerError = InstallScreenBackupError.UnsupportedVersion;
|
||||||
log.error(
|
log.error(
|
||||||
|
@ -178,8 +180,13 @@ 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
|
shouldUnlinkAndDeleteData = true;
|
||||||
await this.#unlinkAndDeleteAllData();
|
} else if (error instanceof BackupImportCanceledError) {
|
||||||
|
installerError = InstallScreenBackupError.Canceled;
|
||||||
|
log.info(
|
||||||
|
'backups.downloadAndImport: Processing canceled by user; unlinking & deleting data'
|
||||||
|
);
|
||||||
|
shouldUnlinkAndDeleteData = true;
|
||||||
} else {
|
} else {
|
||||||
log.error(
|
log.error(
|
||||||
'backups.downloadAndImport: unknown error, prompting user to retry'
|
'backups.downloadAndImport: unknown error, prompting user to retry'
|
||||||
|
@ -191,10 +198,20 @@ export class BackupsService {
|
||||||
error: installerError,
|
error: installerError,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Deleting data takes some time
|
||||||
|
if (shouldUnlinkAndDeleteData) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await this.#unlinkAndDeleteAllData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// For download errors, wait for user confirmation to retry or unlink
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
const nextStep = await this.#downloadRetryPromise.promise;
|
const nextStep = await this.#downloadRetryPromise.promise;
|
||||||
if (nextStep === 'retry') {
|
if (nextStep === 'retry') {
|
||||||
continue;
|
continue;
|
||||||
|
} else if (nextStep === 'cancel') {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await this.#unlinkAndDeleteAllData();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -211,6 +228,7 @@ 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 the primary cancels sync on their end, then we can link without sync
|
||||||
if (!hasBackup) {
|
if (!hasBackup) {
|
||||||
window.reduxActions.installer.handleMissingBackup();
|
window.reduxActions.installer.handleMissingBackup();
|
||||||
}
|
}
|
||||||
|
@ -318,16 +336,27 @@ export class BackupsService {
|
||||||
return this.importBackup(() => createReadStream(backupFile), options);
|
return this.importBackup(() => createReadStream(backupFile), options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public cancelDownload(): void {
|
public cancelDownloadAndImport(): void {
|
||||||
|
if (!this.#downloadController && !this.#importController) {
|
||||||
|
log.error(
|
||||||
|
'cancelDownloadAndImport: not canceling, download or import is not running'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.#downloadController) {
|
if (this.#downloadController) {
|
||||||
log.warn('importBackup: canceling download');
|
log.warn('cancelDownloadAndImport: canceling download');
|
||||||
this.#downloadController.abort();
|
this.#downloadController.abort();
|
||||||
this.#downloadController = undefined;
|
this.#downloadController = undefined;
|
||||||
if (this.#downloadRetryPromise) {
|
if (this.#downloadRetryPromise) {
|
||||||
this.#downloadRetryPromise.resolve('cancel');
|
this.#downloadRetryPromise.resolve('cancel');
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
log.error('importBackup: not canceling download, not running');
|
|
||||||
|
if (this.#importController) {
|
||||||
|
log.warn('cancelDownloadAndImport: canceling import processing');
|
||||||
|
this.#importController.abort();
|
||||||
|
this.#importController = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -350,6 +379,11 @@ export class BackupsService {
|
||||||
await DataWriter.disableMessageInsertTriggers();
|
await DataWriter.disableMessageInsertTriggers();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
this.#importController?.abort();
|
||||||
|
this.#importController = controller;
|
||||||
|
|
||||||
window.ConversationController.setReadOnly(true);
|
window.ConversationController.setReadOnly(true);
|
||||||
|
|
||||||
const importStream = await BackupImportStream.create(backupType);
|
const importStream = await BackupImportStream.create(backupType);
|
||||||
|
@ -378,6 +412,10 @@ export class BackupsService {
|
||||||
sink
|
sink
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (controller.signal.aborted) {
|
||||||
|
throw new BackupImportCanceledError();
|
||||||
|
}
|
||||||
|
|
||||||
onProgress?.(0, totalBytes);
|
onProgress?.(0, totalBytes);
|
||||||
|
|
||||||
strictAssert(theirMac != null, 'importBackup: Missing MAC');
|
strictAssert(theirMac != null, 'importBackup: Missing MAC');
|
||||||
|
@ -405,7 +443,8 @@ export class BackupsService {
|
||||||
getIvAndDecipher(aesKey),
|
getIvAndDecipher(aesKey),
|
||||||
createGunzip(),
|
createGunzip(),
|
||||||
new DelimitedStream(),
|
new DelimitedStream(),
|
||||||
importStream
|
importStream,
|
||||||
|
{ signal: controller.signal }
|
||||||
);
|
);
|
||||||
|
|
||||||
strictAssert(
|
strictAssert(
|
||||||
|
@ -432,7 +471,12 @@ export class BackupsService {
|
||||||
|
|
||||||
log.info('importBackup: finished...');
|
log.info('importBackup: finished...');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.info(`importBackup: failed, error: ${Errors.toLogFormat(error)}`);
|
if (error.name === 'AbortError') {
|
||||||
|
log.info('importBackup: canceled by user');
|
||||||
|
throw new BackupImportCanceledError();
|
||||||
|
}
|
||||||
|
|
||||||
|
log.error(`importBackup: failed, error: ${Errors.toLogFormat(error)}`);
|
||||||
|
|
||||||
if (isNightly(window.getVersion()) || isAdhoc(window.getVersion())) {
|
if (isNightly(window.getVersion()) || isAdhoc(window.getVersion())) {
|
||||||
window.reduxActions.toast.showToast({
|
window.reduxActions.toast.showToast({
|
||||||
|
@ -444,6 +488,8 @@ export class BackupsService {
|
||||||
} finally {
|
} finally {
|
||||||
window.ConversationController.setReadOnly(false);
|
window.ConversationController.setReadOnly(false);
|
||||||
this.#isRunning = false;
|
this.#isRunning = false;
|
||||||
|
this.#importController = undefined;
|
||||||
|
|
||||||
await DataWriter.enableMessageInsertTriggersAndBackfill();
|
await DataWriter.enableMessageInsertTriggersAndBackfill();
|
||||||
|
|
||||||
window.IPC.stopTrackingQueryStats({ epochName: 'Backup Import' });
|
window.IPC.stopTrackingQueryStats({ epochName: 'Backup Import' });
|
||||||
|
@ -531,7 +577,7 @@ export class BackupsService {
|
||||||
await ensureFile(downloadPath);
|
await ensureFile(downloadPath);
|
||||||
|
|
||||||
if (controller.signal.aborted) {
|
if (controller.signal.aborted) {
|
||||||
return false;
|
throw new BackupImportCanceledError();
|
||||||
}
|
}
|
||||||
|
|
||||||
let stream: Readable;
|
let stream: Readable;
|
||||||
|
@ -551,7 +597,7 @@ export class BackupsService {
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (controller.signal.aborted) {
|
if (controller.signal.aborted) {
|
||||||
return false;
|
throw new BackupImportCanceledError();
|
||||||
}
|
}
|
||||||
|
|
||||||
// No backup on the server
|
// No backup on the server
|
||||||
|
@ -580,7 +626,7 @@ export class BackupsService {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (controller.signal.aborted) {
|
if (controller.signal.aborted) {
|
||||||
return false;
|
throw new BackupImportCanceledError();
|
||||||
}
|
}
|
||||||
|
|
||||||
await pipeline(
|
await pipeline(
|
||||||
|
@ -592,14 +638,14 @@ export class BackupsService {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (controller.signal.aborted) {
|
if (controller.signal.aborted) {
|
||||||
return false;
|
throw new BackupImportCanceledError();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#downloadController = undefined;
|
this.#downloadController = undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Too late to cancel now, make sure we are unlinked if the process
|
// Import and start writing to the DB. Make sure we are unlinked
|
||||||
// is aborted due to error or restart.
|
// if the import process is aborted due to error or restart.
|
||||||
const password = window.storage.get('password');
|
const password = window.storage.get('password');
|
||||||
strictAssert(password != null, 'Must be registered to import backup');
|
strictAssert(password != null, 'Must be registered to import backup');
|
||||||
|
|
||||||
|
@ -619,15 +665,19 @@ export class BackupsService {
|
||||||
// Restore password on success
|
// Restore password on success
|
||||||
await window.storage.put('password', password);
|
await window.storage.put('password', password);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Error during import; this is non-retriable
|
// Error or manual cancel during import; this is non-retriable
|
||||||
throw new BackupProcessingError();
|
if (e instanceof BackupImportCanceledError) {
|
||||||
|
throw e;
|
||||||
|
} else {
|
||||||
|
throw new BackupProcessingError();
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
await unlink(downloadPath);
|
await unlink(downloadPath);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Download canceled
|
// Download canceled
|
||||||
if (error.name === 'AbortError') {
|
if (error.name === 'AbortError') {
|
||||||
return false;
|
throw new BackupImportCanceledError();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Other errors bubble up and can be retried
|
// Other errors bubble up and can be retried
|
||||||
|
@ -731,6 +781,10 @@ export class BackupsService {
|
||||||
Errors.toLogFormat(e)
|
Errors.toLogFormat(e)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The QR code should be regenerated only after all data is cleared to prevent
|
||||||
|
// a race where the QR code doesn't show the backup capability
|
||||||
|
window.reduxActions.installer.startInstaller();
|
||||||
}
|
}
|
||||||
|
|
||||||
public isImportRunning(): boolean {
|
public isImportRunning(): boolean {
|
||||||
|
|
|
@ -8,7 +8,6 @@ import { useSelector } from 'react-redux';
|
||||||
import { getIntl } from '../selectors/user';
|
import { getIntl } from '../selectors/user';
|
||||||
import { getUpdatesState } from '../selectors/updates';
|
import { getUpdatesState } from '../selectors/updates';
|
||||||
import { getInstallerState } from '../selectors/installer';
|
import { getInstallerState } from '../selectors/installer';
|
||||||
import { useAppActions } from '../ducks/app';
|
|
||||||
import { useInstallerActions } from '../ducks/installer';
|
import { useInstallerActions } from '../ducks/installer';
|
||||||
import { useUpdatesActions } from '../ducks/updates';
|
import { useUpdatesActions } from '../ducks/updates';
|
||||||
import { hasExpired as hasExpiredSelector } from '../selectors/expiration';
|
import { hasExpired as hasExpiredSelector } from '../selectors/expiration';
|
||||||
|
@ -29,7 +28,6 @@ export const SmartInstallScreen = memo(function SmartInstallScreen() {
|
||||||
const i18n = useSelector(getIntl);
|
const i18n = useSelector(getIntl);
|
||||||
const installerState = useSelector(getInstallerState);
|
const installerState = useSelector(getInstallerState);
|
||||||
const updates = useSelector(getUpdatesState);
|
const updates = useSelector(getUpdatesState);
|
||||||
const { openInbox } = useAppActions();
|
|
||||||
const { startInstaller, finishInstall, retryBackupImport } =
|
const { startInstaller, finishInstall, retryBackupImport } =
|
||||||
useInstallerActions();
|
useInstallerActions();
|
||||||
const { startUpdate, forceUpdate } = useUpdatesActions();
|
const { startUpdate, forceUpdate } = useUpdatesActions();
|
||||||
|
@ -56,11 +54,8 @@ export const SmartInstallScreen = memo(function SmartInstallScreen() {
|
||||||
}, [backupFile, deviceName, finishInstall]);
|
}, [backupFile, deviceName, finishInstall]);
|
||||||
|
|
||||||
const onCancelBackupImport = useCallback((): void => {
|
const onCancelBackupImport = useCallback((): void => {
|
||||||
backupsService.cancelDownload();
|
backupsService.cancelDownloadAndImport();
|
||||||
if (installerState.step === InstallScreenStep.BackupImport) {
|
}, []);
|
||||||
openInbox();
|
|
||||||
}
|
|
||||||
}, [installerState.step, openInbox]);
|
|
||||||
|
|
||||||
const suggestedDeviceName =
|
const suggestedDeviceName =
|
||||||
installerState.step === InstallScreenStep.ChoosingDeviceName
|
installerState.step === InstallScreenStep.ChoosingDeviceName
|
||||||
|
|
|
@ -22,6 +22,7 @@ export enum InstallScreenBackupError {
|
||||||
UnsupportedVersion = 'UnsupportedVersion',
|
UnsupportedVersion = 'UnsupportedVersion',
|
||||||
Retriable = 'Retriable',
|
Retriable = 'Retriable',
|
||||||
Fatal = 'Fatal',
|
Fatal = 'Fatal',
|
||||||
|
Canceled = 'Canceled',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum InstallScreenError {
|
export enum InstallScreenError {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue