Retry dialog for errors during backup download
This commit is contained in:
parent
6e1fd5958e
commit
12f28448b2
6 changed files with 202 additions and 11 deletions
|
@ -4723,6 +4723,34 @@
|
||||||
"messageformat": "Cancel transfer",
|
"messageformat": "Cancel transfer",
|
||||||
"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": {
|
||||||
|
"messageformat": "Error transferring your messages",
|
||||||
|
"description": "Title of the error modal in the backup import screen"
|
||||||
|
},
|
||||||
|
"icu:BackupImportScreen__error__body": {
|
||||||
|
"messageformat": "Your messages could not be transferred. Check your internet connection and try again.",
|
||||||
|
"description": "Body of the error modal in the backup import screen"
|
||||||
|
},
|
||||||
|
"icu:BackupImportScreen__error__confirm": {
|
||||||
|
"messageformat": "Retry",
|
||||||
|
"description": "Text of the retry button of the error modal in the backup import screen"
|
||||||
|
},
|
||||||
|
"icu:BackupImportScreen__skip": {
|
||||||
|
"messageformat": "Skip",
|
||||||
|
"description": "Text of the Skip button in the error and skip confirmation modals in the backup import screen"
|
||||||
|
},
|
||||||
|
"icu:BackupImportScreen__skip-confirmation__title": {
|
||||||
|
"messageformat": "Skip message transfer?",
|
||||||
|
"description": "Title of the cancel confirmation modal in the backup import screen"
|
||||||
|
},
|
||||||
|
"icu:BackupImportScreen__skip-confirmation__body": {
|
||||||
|
"messageformat": "If you choose to skip, you won’t have access to any of your messages or media on this device. You can start a new transfer after skipping from Settings > Chats > Transfer.",
|
||||||
|
"description": "Body of the cancel confirmation modal in the backup import screen"
|
||||||
|
},
|
||||||
|
"icu:BackupImportScreen__skip-confirmation__cancel": {
|
||||||
|
"messageformat": "Cancel",
|
||||||
|
"description": "Text of the cancel button of the skip confirmation modal in the backup import screen"
|
||||||
|
},
|
||||||
"icu:BackupMediaDownloadProgress__title-in-progress": {
|
"icu:BackupMediaDownloadProgress__title-in-progress": {
|
||||||
"messageformat": "Restoring media",
|
"messageformat": "Restoring media",
|
||||||
"description": "Label next to a progress bar showing active media (attachment) download progress after restoring from backup"
|
"description": "Label next to a progress bar showing active media (attachment) download progress after restoring from backup"
|
||||||
|
|
|
@ -21,6 +21,7 @@ const Template: StoryFn<PropsType> = (args: PropsType) => (
|
||||||
{...args}
|
{...args}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
onCancel={action('onCancel')}
|
onCancel={action('onCancel')}
|
||||||
|
onRetry={action('onRetry')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -41,3 +42,10 @@ Full.args = {
|
||||||
currentBytes: 1024,
|
currentBytes: 1024,
|
||||||
totalBytes: 1024,
|
totalBytes: 1024,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const Error = Template.bind({});
|
||||||
|
Error.args = {
|
||||||
|
currentBytes: 500 * 1024,
|
||||||
|
totalBytes: 1024 * 1024,
|
||||||
|
hasError: true,
|
||||||
|
};
|
||||||
|
|
|
@ -18,16 +18,21 @@ export type PropsType = Readonly<{
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
currentBytes?: number;
|
currentBytes?: number;
|
||||||
totalBytes?: number;
|
totalBytes?: number;
|
||||||
|
hasError?: boolean;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
|
onRetry: () => void;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export function InstallScreenBackupImportStep({
|
export function InstallScreenBackupImportStep({
|
||||||
i18n,
|
i18n,
|
||||||
currentBytes,
|
currentBytes,
|
||||||
totalBytes,
|
totalBytes,
|
||||||
|
hasError,
|
||||||
onCancel,
|
onCancel,
|
||||||
|
onRetry,
|
||||||
}: PropsType): JSX.Element {
|
}: PropsType): JSX.Element {
|
||||||
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);
|
||||||
|
@ -42,6 +47,24 @@ export function InstallScreenBackupImportStep({
|
||||||
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(() => {
|
||||||
|
onRetry();
|
||||||
|
setIsConfirmingSkip(false);
|
||||||
|
}, [onRetry]);
|
||||||
|
|
||||||
let progress: JSX.Element;
|
let progress: JSX.Element;
|
||||||
let isCancelPossible = true;
|
let isCancelPossible = true;
|
||||||
if (currentBytes != null && totalBytes != null) {
|
if (currentBytes != null && totalBytes != null) {
|
||||||
|
@ -79,6 +102,7 @@ export function InstallScreenBackupImportStep({
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="InstallScreenBackupImportStep">
|
<div className="InstallScreenBackupImportStep">
|
||||||
<TitlebarDragArea />
|
<TitlebarDragArea />
|
||||||
|
@ -127,6 +151,44 @@ export function InstallScreenBackupImportStep({
|
||||||
{i18n('icu:BackupImportScreen__cancel-confirmation__body')}
|
{i18n('icu:BackupImportScreen__cancel-confirmation__body')}
|
||||||
</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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasError && !isConfirmingSkip && (
|
||||||
|
<ConfirmationDialog
|
||||||
|
dialogName="InstallScreenBackupImportStep.error"
|
||||||
|
title={i18n('icu:BackupImportScreen__error__title')}
|
||||||
|
cancelText={i18n('icu:BackupImportScreen__skip')}
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
action: onRetryWrap,
|
||||||
|
style: 'affirmative',
|
||||||
|
text: i18n('icu:BackupImportScreen__error__confirm'),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
i18n={i18n}
|
||||||
|
onClose={confirmSkip}
|
||||||
|
>
|
||||||
|
{i18n('icu:BackupImportScreen__error__body')}
|
||||||
|
</ConfirmationDialog>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,9 @@ import { BackupCredentials } from './credentials';
|
||||||
import { BackupAPI, type DownloadOptionsType } from './api';
|
import { BackupAPI, type DownloadOptionsType } from './api';
|
||||||
import { validateBackup } from './validator';
|
import { validateBackup } from './validator';
|
||||||
import { BackupType } from './types';
|
import { BackupType } from './types';
|
||||||
|
import type { ExplodePromiseResultType } from '../../util/explodePromise';
|
||||||
|
import { explodePromise } from '../../util/explodePromise';
|
||||||
|
import type { RetryBackupImportValue } from '../../state/ducks/installer';
|
||||||
|
|
||||||
export { BackupType };
|
export { BackupType };
|
||||||
|
|
||||||
|
@ -50,6 +53,9 @@ export class BackupsService {
|
||||||
private isStarted = false;
|
private isStarted = false;
|
||||||
private isRunning = false;
|
private isRunning = false;
|
||||||
private downloadController: AbortController | undefined;
|
private downloadController: AbortController | undefined;
|
||||||
|
private downloadRetryPromise:
|
||||||
|
| ExplodePromiseResultType<RetryBackupImportValue>
|
||||||
|
| undefined;
|
||||||
|
|
||||||
public readonly credentials = new BackupCredentials();
|
public readonly credentials = new BackupCredentials();
|
||||||
public readonly api = new BackupAPI(this.credentials);
|
public readonly api = new BackupAPI(this.credentials);
|
||||||
|
@ -87,14 +93,50 @@ export class BackupsService {
|
||||||
|
|
||||||
const absoluteDownloadPath =
|
const absoluteDownloadPath =
|
||||||
window.Signal.Migrations.getAbsoluteDownloadsPath(backupDownloadPath);
|
window.Signal.Migrations.getAbsoluteDownloadsPath(backupDownloadPath);
|
||||||
|
let hasBackup = false;
|
||||||
log.info('backups.download: downloading...');
|
log.info('backups.download: downloading...');
|
||||||
const hasBackup = await this.doDownload(absoluteDownloadPath, options);
|
|
||||||
|
// eslint-disable-next-line no-constant-condition
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
hasBackup = await this.doDownload(absoluteDownloadPath, options);
|
||||||
|
} catch (error) {
|
||||||
|
log.info('backups.download: error, prompting user to retry');
|
||||||
|
this.downloadRetryPromise = explodePromise<RetryBackupImportValue>();
|
||||||
|
window.reduxActions.installer.updateBackupImportProgress({
|
||||||
|
hasError: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const nextStep = await this.downloadRetryPromise.promise;
|
||||||
|
if (nextStep === 'retry') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await unlink(absoluteDownloadPath);
|
||||||
|
} catch {
|
||||||
|
// Best-effort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
await window.storage.remove('backupDownloadPath');
|
await window.storage.remove('backupDownloadPath');
|
||||||
|
|
||||||
log.info(`backups.download: done, had backup=${hasBackup}`);
|
log.info(`backups.download: done, had backup=${hasBackup}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public retryDownload(): void {
|
||||||
|
if (!this.downloadRetryPromise) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.downloadRetryPromise.resolve('retry');
|
||||||
|
}
|
||||||
|
|
||||||
public async upload(): Promise<void> {
|
public async upload(): Promise<void> {
|
||||||
const fileName = `backup-${randomBytes(32).toString('hex')}`;
|
const fileName = `backup-${randomBytes(32).toString('hex')}`;
|
||||||
const filePath = join(window.BasePaths.temp, fileName);
|
const filePath = join(window.BasePaths.temp, fileName);
|
||||||
|
@ -169,6 +211,9 @@ export class BackupsService {
|
||||||
log.warn('importBackup: canceling download');
|
log.warn('importBackup: canceling download');
|
||||||
this.downloadController.abort();
|
this.downloadController.abort();
|
||||||
this.downloadController = undefined;
|
this.downloadController = undefined;
|
||||||
|
if (this.downloadRetryPromise) {
|
||||||
|
this.downloadRetryPromise.resolve('cancel');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
log.error('importBackup: not canceling download, not running');
|
log.error('importBackup: not canceling download, not running');
|
||||||
}
|
}
|
||||||
|
@ -365,11 +410,7 @@ export class BackupsService {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// Other errors bubble up and can be retried
|
||||||
await unlink(downloadPath);
|
|
||||||
} catch {
|
|
||||||
// Best-effort
|
|
||||||
}
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,7 @@ import {
|
||||||
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
|
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
|
||||||
import { useBoundActions } from '../../hooks/useBoundActions';
|
import { useBoundActions } from '../../hooks/useBoundActions';
|
||||||
import * as log from '../../logging/log';
|
import * as log from '../../logging/log';
|
||||||
|
import { backupsService } from '../../services/backups';
|
||||||
|
|
||||||
const SLEEP_ERROR = new TimeoutError();
|
const SLEEP_ERROR = new TimeoutError();
|
||||||
|
|
||||||
|
@ -63,14 +64,18 @@ export type InstallerStateType = ReadonlyDeep<
|
||||||
step: InstallScreenStep.BackupImport;
|
step: InstallScreenStep.BackupImport;
|
||||||
currentBytes?: number;
|
currentBytes?: number;
|
||||||
totalBytes?: number;
|
totalBytes?: number;
|
||||||
|
hasError?: boolean;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export type RetryBackupImportValue = ReadonlyDeep<'retry' | 'cancel'>;
|
||||||
|
|
||||||
export const START_INSTALLER = 'installer/START_INSTALLER';
|
export const START_INSTALLER = 'installer/START_INSTALLER';
|
||||||
const SET_PROVISIONING_URL = 'installer/SET_PROVISIONING_URL';
|
const SET_PROVISIONING_URL = 'installer/SET_PROVISIONING_URL';
|
||||||
const SET_QR_CODE_ERROR = 'installer/SET_QR_CODE_ERROR';
|
const SET_QR_CODE_ERROR = 'installer/SET_QR_CODE_ERROR';
|
||||||
const SET_ERROR = 'installer/SET_ERROR';
|
const SET_ERROR = 'installer/SET_ERROR';
|
||||||
const QR_CODE_SCANNED = 'installer/QR_CODE_SCANNED';
|
const QR_CODE_SCANNED = 'installer/QR_CODE_SCANNED';
|
||||||
|
const RETRY_BACKUP_IMPORT = 'installer/RETRY_BACKUP_IMPORT';
|
||||||
const SHOW_LINK_IN_PROGRESS = 'installer/SHOW_LINK_IN_PROGRESS';
|
const SHOW_LINK_IN_PROGRESS = 'installer/SHOW_LINK_IN_PROGRESS';
|
||||||
export const SHOW_BACKUP_IMPORT = 'installer/SHOW_BACKUP_IMPORT';
|
export const SHOW_BACKUP_IMPORT = 'installer/SHOW_BACKUP_IMPORT';
|
||||||
const UPDATE_BACKUP_IMPORT_PROGRESS = 'installer/UPDATE_BACKUP_IMPORT_PROGRESS';
|
const UPDATE_BACKUP_IMPORT_PROGRESS = 'installer/UPDATE_BACKUP_IMPORT_PROGRESS';
|
||||||
|
@ -103,6 +108,10 @@ type QRCodeScannedActionType = ReadonlyDeep<{
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
type RetryBackupImportActionType = ReadonlyDeep<{
|
||||||
|
type: typeof RETRY_BACKUP_IMPORT;
|
||||||
|
}>;
|
||||||
|
|
||||||
type ShowLinkInProgressActionType = ReadonlyDeep<{
|
type ShowLinkInProgressActionType = ReadonlyDeep<{
|
||||||
type: typeof SHOW_LINK_IN_PROGRESS;
|
type: typeof SHOW_LINK_IN_PROGRESS;
|
||||||
}>;
|
}>;
|
||||||
|
@ -113,9 +122,13 @@ export type ShowBackupImportActionType = ReadonlyDeep<{
|
||||||
|
|
||||||
type UpdateBackupImportProgressActionType = ReadonlyDeep<{
|
type UpdateBackupImportProgressActionType = ReadonlyDeep<{
|
||||||
type: typeof UPDATE_BACKUP_IMPORT_PROGRESS;
|
type: typeof UPDATE_BACKUP_IMPORT_PROGRESS;
|
||||||
payload: {
|
payload:
|
||||||
|
| {
|
||||||
currentBytes: number;
|
currentBytes: number;
|
||||||
totalBytes: number;
|
totalBytes: number;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
hasError: boolean;
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
@ -125,6 +138,7 @@ export type InstallerActionType = ReadonlyDeep<
|
||||||
| SetQRCodeErrorActionType
|
| SetQRCodeErrorActionType
|
||||||
| SetErrorActionType
|
| SetErrorActionType
|
||||||
| QRCodeScannedActionType
|
| QRCodeScannedActionType
|
||||||
|
| RetryBackupImportActionType
|
||||||
| ShowLinkInProgressActionType
|
| ShowLinkInProgressActionType
|
||||||
| ShowBackupImportActionType
|
| ShowBackupImportActionType
|
||||||
| UpdateBackupImportProgressActionType
|
| UpdateBackupImportProgressActionType
|
||||||
|
@ -134,6 +148,7 @@ export const actions = {
|
||||||
startInstaller,
|
startInstaller,
|
||||||
finishInstall,
|
finishInstall,
|
||||||
updateBackupImportProgress,
|
updateBackupImportProgress,
|
||||||
|
retryBackupImport,
|
||||||
showBackupImport,
|
showBackupImport,
|
||||||
showLinkInProgress,
|
showLinkInProgress,
|
||||||
};
|
};
|
||||||
|
@ -414,6 +429,18 @@ function updateBackupImportProgress(
|
||||||
return { type: UPDATE_BACKUP_IMPORT_PROGRESS, payload };
|
return { type: UPDATE_BACKUP_IMPORT_PROGRESS, payload };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function retryBackupImport(): ThunkAction<
|
||||||
|
void,
|
||||||
|
RootStateType,
|
||||||
|
unknown,
|
||||||
|
RetryBackupImportActionType
|
||||||
|
> {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch({ type: RETRY_BACKUP_IMPORT });
|
||||||
|
backupsService.retryDownload();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Reducer
|
// Reducer
|
||||||
|
|
||||||
export function getEmptyState(): InstallerStateType {
|
export function getEmptyState(): InstallerStateType {
|
||||||
|
@ -546,6 +573,13 @@ export function reducer(
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ('hasError' in action.payload) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
hasError: action.payload.hasError,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
currentBytes: action.payload.currentBytes,
|
currentBytes: action.payload.currentBytes,
|
||||||
|
@ -553,5 +587,20 @@ export function reducer(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action.type === RETRY_BACKUP_IMPORT) {
|
||||||
|
if (state.step !== InstallScreenStep.BackupImport) {
|
||||||
|
log.warn(
|
||||||
|
'ducks/installer: wrong step, not retrying backup import',
|
||||||
|
state.step
|
||||||
|
);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
hasError: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,8 @@ export const SmartInstallScreen = memo(function SmartInstallScreen() {
|
||||||
const installerState = useSelector(getInstallerState);
|
const installerState = useSelector(getInstallerState);
|
||||||
const updates = useSelector(getUpdatesState);
|
const updates = useSelector(getUpdatesState);
|
||||||
const { openInbox } = useAppActions();
|
const { openInbox } = useAppActions();
|
||||||
const { startInstaller, finishInstall } = useInstallerActions();
|
const { startInstaller, finishInstall, retryBackupImport } =
|
||||||
|
useInstallerActions();
|
||||||
const { startUpdate } = useUpdatesActions();
|
const { startUpdate } = useUpdatesActions();
|
||||||
const hasExpired = useSelector(hasExpiredSelector);
|
const hasExpired = useSelector(hasExpiredSelector);
|
||||||
|
|
||||||
|
@ -110,7 +111,9 @@ export const SmartInstallScreen = memo(function SmartInstallScreen() {
|
||||||
i18n,
|
i18n,
|
||||||
currentBytes: installerState.currentBytes,
|
currentBytes: installerState.currentBytes,
|
||||||
totalBytes: installerState.totalBytes,
|
totalBytes: installerState.totalBytes,
|
||||||
|
hasError: installerState.hasError,
|
||||||
onCancel: onCancelBackupImport,
|
onCancel: onCancelBackupImport,
|
||||||
|
onRetry: retryBackupImport,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
|
Loading…
Reference in a new issue