mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-02-15 07:28:59 +00:00
Show low-disk-space warning during backup media download
This commit is contained in:
@@ -5348,6 +5348,14 @@
|
||||
"messageformat": "You must open Signal on your phone to continue using this account. If you do not open Signal on your phone, your account will be deleted soon. <learnMoreLink>Learn more</learnMoreLink>",
|
||||
"description": "Description for modal shown when a user must use their idle primary device to retain their account. Learn more link will link to a support page"
|
||||
},
|
||||
"icu:LowDiskSpaceBackupImportModal__title": {
|
||||
"messageformat": "Can't restore media",
|
||||
"description": "Title for modal shown when there is insufficient disk space to download media from backup"
|
||||
},
|
||||
"icu:LowDiskSpaceBackupImportModal__description": {
|
||||
"messageformat": "Your device does not have enough free space. Free up {diskSpaceAmount} of space to restore your media.",
|
||||
"description": "Description for modal shown when there is insufficient disk space to download media from backup. Example {diskSpaceAmount}: '1.23 GB', '512 MB'"
|
||||
},
|
||||
"icu:CompositionArea--expand": {
|
||||
"messageformat": "Expand",
|
||||
"description": "Aria label for expanding composition area"
|
||||
|
||||
5
images/icons/v3/backup/backup-warning.svg
Normal file
5
images/icons/v3/backup/backup-warning.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
|
||||
<path d="M16.0001 4.38645C9.58605 4.38645 4.38645 9.58605 4.38645 16.0001C4.38645 18.9786 5.50643 21.6937 7.35037 23.75L7.95575 23.1447C8.47875 22.6217 9.37218 22.8665 9.55548 23.5831L10.5215 27.3591C10.7012 28.0617 10.0627 28.7002 9.36012 28.5205L5.58404 27.5545C4.86748 27.3712 4.62264 26.4778 5.14564 25.9548L5.7735 25.3269C3.52938 22.8675 2.15918 19.5929 2.15918 16.0001C2.15918 8.35597 8.35597 2.15918 16.0001 2.15918C23.6442 2.15918 29.841 8.35597 29.841 16.0001C29.841 23.6442 23.6442 29.841 16.0001 29.841C15.385 29.841 14.8865 29.3424 14.8865 28.7274C14.8865 28.1123 15.385 27.6137 16.0001 27.6137C22.4141 27.6137 27.6137 22.4141 27.6137 16.0001C27.6137 9.58605 22.4141 4.38645 16.0001 4.38645Z" fill="black" style="fill:black;fill-opacity:1;"/>
|
||||
<path d="M14.8027 16.1104C14.8449 16.8834 15.2524 17.3051 15.9973 17.3051C16.7282 17.3051 17.1357 16.8975 17.1639 16.1104L17.3747 11.36C17.4028 10.5308 16.7984 9.95459 15.9833 9.95459C15.1681 9.95459 14.5778 10.5168 14.62 11.346L14.8027 16.1104Z" fill="black" style="fill:black;fill-opacity:1;"/>
|
||||
<path d="M14.4092 20.2846C14.4092 21.1138 15.0978 21.7603 15.9973 21.7603C16.8687 21.7603 17.5714 21.1279 17.5714 20.2846C17.5714 19.4273 16.8828 18.7948 15.9973 18.7948C15.0978 18.7948 14.4092 19.4273 14.4092 20.2846Z" fill="black" style="fill:black;fill-opacity:1;"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
52
stylesheets/components/LowDiskSpaceBackupImportModal.scss
Normal file
52
stylesheets/components/LowDiskSpaceBackupImportModal.scss
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
@use '../mixins';
|
||||
@use '../variables';
|
||||
|
||||
.LowDiskSpaceBackupImportModal__content {
|
||||
max-width: 440px;
|
||||
padding-block: 12px;
|
||||
padding-inline: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.LowDiskSpaceBackupImportModal__icon {
|
||||
background-color: variables.$color-accent-yellow;
|
||||
padding-block: 12px;
|
||||
padding-inline: 12px;
|
||||
border-radius: 50%;
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
@include mixins.color-svg(
|
||||
'../images/icons/v3/backup/backup-warning.svg',
|
||||
variables.$color-black
|
||||
);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.LowDiskSpaceBackupImportModal__header {
|
||||
@include mixins.font-title-medium;
|
||||
margin-block: 12px;
|
||||
}
|
||||
|
||||
.LowDiskSpaceBackupImportModal__description {
|
||||
margin-block: 12px;
|
||||
margin-inline: 8px;
|
||||
color: light-dark(variables.$color-gray-60, variables.$color-gray-25);
|
||||
}
|
||||
|
||||
.LowDiskSpaceBackupImportModal__button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-block-start: 18px;
|
||||
|
||||
button {
|
||||
padding-inline: 26px;
|
||||
}
|
||||
}
|
||||
@@ -125,6 +125,7 @@
|
||||
@use 'components/Lightbox.scss';
|
||||
@use 'components/ListTile.scss';
|
||||
@use 'components/LocalDeleteWarningModal.scss';
|
||||
@use 'components/LowDiskSpaceBackupImportModal.scss';
|
||||
@use 'components/MediaEditor.scss';
|
||||
@use 'components/MediaPermissionsModal.scss';
|
||||
@use 'components/MediaQualitySelector.scss';
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
} from './BackfillFailureModal';
|
||||
import type { SmartDraftGifMessageSendModalProps } from '../state/smart/DraftGifMessageSendModal';
|
||||
import { CriticalIdlePrimaryDeviceModal } from './CriticalIdlePrimaryDeviceModal';
|
||||
import { LowDiskSpaceBackupImportModal } from './LowDiskSpaceBackupImportModal';
|
||||
|
||||
// NOTE: All types should be required for this component so that the smart
|
||||
// component gives you type errors when adding/removing props.
|
||||
@@ -151,6 +152,9 @@ export type PropsType = {
|
||||
// CriticalIdlePrimaryDeviceModal,
|
||||
criticalIdlePrimaryDeviceModal: boolean;
|
||||
hideCriticalIdlePrimaryDeviceModal: () => void;
|
||||
// LowDiskSpaceBackupImportModal
|
||||
lowDiskSpaceBackupImportModal: { bytesNeeded: number } | null;
|
||||
hideLowDiskSpaceBackupImportModal: () => void;
|
||||
};
|
||||
|
||||
export function GlobalModalContainer({
|
||||
@@ -250,6 +254,9 @@ export function GlobalModalContainer({
|
||||
// CriticalIdlePrimaryDeviceModal
|
||||
criticalIdlePrimaryDeviceModal,
|
||||
hideCriticalIdlePrimaryDeviceModal,
|
||||
// LowDiskSpaceBackupImportModal
|
||||
lowDiskSpaceBackupImportModal,
|
||||
hideLowDiskSpaceBackupImportModal,
|
||||
}: PropsType): JSX.Element | null {
|
||||
// We want the following dialogs to show in this order:
|
||||
// 1. Errors
|
||||
@@ -441,5 +448,15 @@ export function GlobalModalContainer({
|
||||
);
|
||||
}
|
||||
|
||||
if (lowDiskSpaceBackupImportModal) {
|
||||
return (
|
||||
<LowDiskSpaceBackupImportModal
|
||||
bytesNeeded={lowDiskSpaceBackupImportModal.bytesNeeded}
|
||||
i18n={i18n}
|
||||
onClose={hideLowDiskSpaceBackupImportModal}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
31
ts/components/LowDiskSpaceBackupImportModal.stories.tsx
Normal file
31
ts/components/LowDiskSpaceBackupImportModal.stories.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import React, { type ComponentProps } from 'react';
|
||||
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { LowDiskSpaceBackupImportModal } from './LowDiskSpaceBackupImportModal';
|
||||
import { MEBIBYTE } from '../types/AttachmentSize';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
type PropsType = ComponentProps<typeof LowDiskSpaceBackupImportModal>;
|
||||
|
||||
export default {
|
||||
title: 'Components/LowDiskSpaceBackupImportModal',
|
||||
component: LowDiskSpaceBackupImportModal,
|
||||
args: {
|
||||
i18n,
|
||||
onClose: action('close'),
|
||||
bytesNeeded: 1540 * MEBIBYTE,
|
||||
},
|
||||
} satisfies Meta<PropsType>;
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
const Template: StoryFn<PropsType> = args => (
|
||||
<LowDiskSpaceBackupImportModal {...args} />
|
||||
);
|
||||
|
||||
export const Modal = Template.bind({});
|
||||
Modal.args = {};
|
||||
48
ts/components/LowDiskSpaceBackupImportModal.tsx
Normal file
48
ts/components/LowDiskSpaceBackupImportModal.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { Modal } from './Modal';
|
||||
import { Button, ButtonVariant } from './Button';
|
||||
import { formatFileSize } from '../util/formatFileSize';
|
||||
|
||||
export type PropsType = Readonly<{
|
||||
bytesNeeded: number;
|
||||
i18n: LocalizerType;
|
||||
onClose: () => void;
|
||||
}>;
|
||||
|
||||
export function LowDiskSpaceBackupImportModal(props: PropsType): JSX.Element {
|
||||
const { i18n, bytesNeeded, onClose } = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
modalName="LowDiskSpaceBackupImportModal"
|
||||
moduleClassName="LowDiskSpaceBackupImportModal"
|
||||
i18n={i18n}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className="LowDiskSpaceBackupImportModal__content">
|
||||
<div className="LowDiskSpaceBackupImportModal__icon" />
|
||||
|
||||
<div className="LowDiskSpaceBackupImportModal__header">
|
||||
{i18n('icu:LowDiskSpaceBackupImportModal__title')}
|
||||
</div>
|
||||
|
||||
<div className="LowDiskSpaceBackupImportModal__description">
|
||||
{i18n('icu:LowDiskSpaceBackupImportModal__description', {
|
||||
diskSpaceAmount: formatFileSize(bytesNeeded),
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="LowDiskSpaceBackupImportModal__button">
|
||||
<Button onClick={onClose} variant={ButtonVariant.Primary}>
|
||||
{i18n('icu:Confirmation--confirm')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import { noop, omit, throttle } from 'lodash';
|
||||
import { statfs } from 'node:fs/promises';
|
||||
|
||||
import * as durations from '../util/durations';
|
||||
import * as log from '../logging/log';
|
||||
@@ -61,6 +62,7 @@ import {
|
||||
isPermanentlyUndownloadable,
|
||||
isPermanentlyUndownloadableWithoutBackfill,
|
||||
} from './helpers/attachmentBackfill';
|
||||
import { formatCountForLogging } from '../logging/formatCountForLogging';
|
||||
|
||||
export { isPermanentlyUndownloadable };
|
||||
|
||||
@@ -91,6 +93,14 @@ const BACKUP_RETRY_CONFIG = {
|
||||
...DEFAULT_RETRY_CONFIG,
|
||||
maxAttempts: Infinity,
|
||||
};
|
||||
|
||||
type RunDownloadAttachmentJobOptions = {
|
||||
abortSignal: AbortSignal;
|
||||
isForCurrentlyVisibleMessage: boolean;
|
||||
maxAttachmentSizeInKib: number;
|
||||
maxTextAttachmentSizeInKib: number;
|
||||
};
|
||||
|
||||
type AttachmentDownloadManagerParamsType = Omit<
|
||||
JobManagerParamsType<CoreAttachmentDownloadJobType>,
|
||||
'getNextJobs' | 'runJob'
|
||||
@@ -104,12 +114,11 @@ type AttachmentDownloadManagerParamsType = Omit<
|
||||
runDownloadAttachmentJob: (args: {
|
||||
job: AttachmentDownloadJobType;
|
||||
isLastAttempt: boolean;
|
||||
options: {
|
||||
abortSignal: AbortSignal;
|
||||
isForCurrentlyVisibleMessage: boolean;
|
||||
};
|
||||
options: RunDownloadAttachmentJobOptions;
|
||||
dependencies?: DependenciesType;
|
||||
}) => Promise<JobManagerJobResultType<CoreAttachmentDownloadJobType>>;
|
||||
onLowDiskSpaceBackupImport: (bytesNeeded: number) => Promise<void>;
|
||||
statfs: typeof statfs;
|
||||
};
|
||||
|
||||
function getJobId(job: CoreAttachmentDownloadJobType): string {
|
||||
@@ -135,6 +144,13 @@ export class AttachmentDownloadManager extends JobManager<CoreAttachmentDownload
|
||||
drop(this.maybeStartJobs());
|
||||
},
|
||||
});
|
||||
#onLowDiskSpaceBackupImport: (bytesNeeded: number) => Promise<void>;
|
||||
#statfs: typeof statfs;
|
||||
#maxAttachmentSizeInKib = getMaximumIncomingAttachmentSizeInKb(getValue);
|
||||
#maxTextAttachmentSizeInKib =
|
||||
getMaximumIncomingTextAttachmentSizeInKb(getValue);
|
||||
|
||||
#minimumFreeDiskSpace = this.#maxAttachmentSizeInKib * 5;
|
||||
|
||||
#attachmentBackfill = new AttachmentBackfill();
|
||||
|
||||
@@ -169,6 +185,19 @@ export class AttachmentDownloadManager extends JobManager<CoreAttachmentDownload
|
||||
? BACKUP_RETRY_CONFIG
|
||||
: DEFAULT_RETRY_CONFIG,
|
||||
maxConcurrentJobs: MAX_CONCURRENT_JOBS,
|
||||
onLowDiskSpaceBackupImport: async bytesNeeded => {
|
||||
if (!window.storage.get('backupMediaDownloadPaused')) {
|
||||
await Promise.all([
|
||||
window.storage.put('backupMediaDownloadPaused', true),
|
||||
// Show the banner to allow users to resume from the left pane
|
||||
window.storage.put('backupMediaDownloadBannerDismissed', false),
|
||||
]);
|
||||
}
|
||||
window.reduxActions.globalModals.showLowDiskSpaceBackupImportModal(
|
||||
bytesNeeded
|
||||
);
|
||||
},
|
||||
statfs,
|
||||
};
|
||||
|
||||
constructor(params: AttachmentDownloadManagerParamsType) {
|
||||
@@ -184,7 +213,7 @@ export class AttachmentDownloadManager extends JobManager<CoreAttachmentDownload
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
},
|
||||
runJob: (
|
||||
runJob: async (
|
||||
job: AttachmentDownloadJobType,
|
||||
{
|
||||
abortSignal,
|
||||
@@ -194,16 +223,29 @@ export class AttachmentDownloadManager extends JobManager<CoreAttachmentDownload
|
||||
const isForCurrentlyVisibleMessage = this.#visibleTimelineMessages.has(
|
||||
job.messageId
|
||||
);
|
||||
|
||||
if (job.source === AttachmentDownloadSource.BACKUP_IMPORT) {
|
||||
const { outOfSpace } =
|
||||
await this.#checkFreeDiskSpaceForBackupImport();
|
||||
if (outOfSpace) {
|
||||
return { status: 'retry' };
|
||||
}
|
||||
}
|
||||
|
||||
return params.runDownloadAttachmentJob({
|
||||
job,
|
||||
isLastAttempt,
|
||||
options: {
|
||||
abortSignal,
|
||||
isForCurrentlyVisibleMessage,
|
||||
maxAttachmentSizeInKib: this.#maxAttachmentSizeInKib,
|
||||
maxTextAttachmentSizeInKib: this.#maxTextAttachmentSizeInKib,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
this.#onLowDiskSpaceBackupImport = params.onLowDiskSpaceBackupImport;
|
||||
this.#statfs = params.statfs;
|
||||
}
|
||||
|
||||
// @ts-expect-error we are overriding the return type of JobManager's addJob
|
||||
@@ -266,6 +308,48 @@ export class AttachmentDownloadManager extends JobManager<CoreAttachmentDownload
|
||||
this.#visibleTimelineMessages = new Set(messageIds);
|
||||
}
|
||||
|
||||
async #getFreeDiskSpace(): Promise<number> {
|
||||
const { bsize, bavail } = await this.#statfs(
|
||||
window.SignalContext.getPath('userData')
|
||||
);
|
||||
return bsize * bavail;
|
||||
}
|
||||
|
||||
async #checkFreeDiskSpaceForBackupImport(): Promise<{
|
||||
outOfSpace: boolean;
|
||||
}> {
|
||||
let freeDiskSpace: number;
|
||||
|
||||
try {
|
||||
freeDiskSpace = await this.#getFreeDiskSpace();
|
||||
} catch (e) {
|
||||
log.error(
|
||||
'checkFreeDiskSpaceForBackupImport: error checking disk space',
|
||||
Errors.toLogFormat(e)
|
||||
);
|
||||
// Still attempt the download
|
||||
return { outOfSpace: false };
|
||||
}
|
||||
|
||||
if (freeDiskSpace <= this.#minimumFreeDiskSpace) {
|
||||
const remainingBackupBytesToDownload =
|
||||
window.storage.get('backupMediaDownloadTotalBytes', 0) -
|
||||
window.storage.get('backupMediaDownloadCompletedBytes', 0);
|
||||
|
||||
log.info(
|
||||
'AttachmentDownloadManager.checkFreeDiskSpaceForBackupImport: insufficient disk space. ' +
|
||||
`Available: ${formatCountForLogging(freeDiskSpace)}, ` +
|
||||
`Needed: ${formatCountForLogging(remainingBackupBytesToDownload)} ` +
|
||||
`Minimum threshold: ${this.#minimumFreeDiskSpace}`
|
||||
);
|
||||
|
||||
await this.#onLowDiskSpaceBackupImport(remainingBackupBytesToDownload);
|
||||
return { outOfSpace: true };
|
||||
}
|
||||
|
||||
return { outOfSpace: false };
|
||||
}
|
||||
|
||||
static get instance(): AttachmentDownloadManager {
|
||||
if (!AttachmentDownloadManager._instance) {
|
||||
AttachmentDownloadManager._instance = new AttachmentDownloadManager(
|
||||
@@ -345,10 +429,7 @@ async function runDownloadAttachmentJob({
|
||||
}: {
|
||||
job: AttachmentDownloadJobType;
|
||||
isLastAttempt: boolean;
|
||||
options: {
|
||||
abortSignal: AbortSignal;
|
||||
isForCurrentlyVisibleMessage: boolean;
|
||||
};
|
||||
options: RunDownloadAttachmentJobOptions;
|
||||
dependencies?: DependenciesType;
|
||||
}): Promise<JobManagerJobResultType<CoreAttachmentDownloadJobType>> {
|
||||
const jobIdForLogging = getJobIdForLogging(job);
|
||||
@@ -369,6 +450,8 @@ async function runDownloadAttachmentJob({
|
||||
abortSignal: options.abortSignal,
|
||||
isForCurrentlyVisibleMessage:
|
||||
options?.isForCurrentlyVisibleMessage ?? false,
|
||||
maxAttachmentSizeInKib: options.maxAttachmentSizeInKib,
|
||||
maxTextAttachmentSizeInKib: options.maxTextAttachmentSizeInKib,
|
||||
dependencies,
|
||||
});
|
||||
|
||||
@@ -491,13 +574,13 @@ export async function runDownloadAttachmentJobInner({
|
||||
job,
|
||||
abortSignal,
|
||||
isForCurrentlyVisibleMessage,
|
||||
maxAttachmentSizeInKib,
|
||||
maxTextAttachmentSizeInKib,
|
||||
dependencies,
|
||||
}: {
|
||||
job: AttachmentDownloadJobType;
|
||||
abortSignal: AbortSignal;
|
||||
isForCurrentlyVisibleMessage: boolean;
|
||||
dependencies: DependenciesType;
|
||||
}): Promise<DownloadAttachmentResultType> {
|
||||
} & RunDownloadAttachmentJobOptions): Promise<DownloadAttachmentResultType> {
|
||||
const { messageId, attachment, attachmentType } = job;
|
||||
|
||||
const jobIdForLogging = getJobIdForLogging(job);
|
||||
@@ -507,16 +590,16 @@ export async function runDownloadAttachmentJobInner({
|
||||
throw new Error(`${logId}: Key information required for job was missing.`);
|
||||
}
|
||||
|
||||
const maxInKib = getMaximumIncomingAttachmentSizeInKb(getValue);
|
||||
const maxTextAttachmentSizeInKib =
|
||||
getMaximumIncomingTextAttachmentSizeInKb(getValue);
|
||||
|
||||
const { size } = attachment;
|
||||
const sizeInKib = size / KIBIBYTE;
|
||||
|
||||
if (!Number.isFinite(size) || size < 0 || sizeInKib > maxInKib) {
|
||||
if (
|
||||
!Number.isFinite(size) ||
|
||||
size < 0 ||
|
||||
sizeInKib > maxAttachmentSizeInKib
|
||||
) {
|
||||
throw new AttachmentSizeError(
|
||||
`${logId}: Attachment was ${sizeInKib}kib, max is ${maxInKib}kib`
|
||||
`${logId}: Attachment was ${sizeInKib}kib, max is ${maxAttachmentSizeInKib}kib`
|
||||
);
|
||||
}
|
||||
if (
|
||||
|
||||
@@ -132,6 +132,9 @@ export type GlobalModalsStateType = ReadonlyDeep<{
|
||||
isSignalConnectionsVisible: boolean;
|
||||
isStoriesSettingsVisible: boolean;
|
||||
isWhatsNewVisible: boolean;
|
||||
lowDiskSpaceBackupImportModal: {
|
||||
bytesNeeded: number;
|
||||
} | null;
|
||||
messageRequestActionsConfirmationProps: MessageRequestActionsConfirmationPropsType | null;
|
||||
notePreviewModalProps: NotePreviewModalPropsType | null;
|
||||
usernameOnboardingState: UsernameOnboardingState;
|
||||
@@ -222,6 +225,10 @@ const SHOW_CRITICAL_IDLE_PRIMARY_DEVICE_MODAL =
|
||||
'globalModals/SHOW_CRITICAL_IDLE_PRIMARY_DEVICE_MODAL';
|
||||
const HIDE_CRITICAL_IDLE_PRIMARY_DEVICE_MODAL =
|
||||
'globalModals/HIDE_CRITICAL_IDLE_PRIMARY_DEVICE_MODAL';
|
||||
const SHOW_LOW_DISK_SPACE_BACKUP_IMPORT_MODAL =
|
||||
'globalModals/SHOW_LOW_DISK_SPACE_BACKUP_IMPORT_MODAL';
|
||||
const HIDE_LOW_DISK_SPACE_BACKUP_IMPORT_MODAL =
|
||||
'globalModals/HIDE_LOW_DISK_SPACE_BACKUP_IMPORT_MODAL';
|
||||
|
||||
export type ContactModalStateType = ReadonlyDeep<{
|
||||
contactId: string;
|
||||
@@ -449,6 +456,17 @@ type HideCriticalIdlePrimaryDeviceModalActionType = ReadonlyDeep<{
|
||||
type: typeof HIDE_CRITICAL_IDLE_PRIMARY_DEVICE_MODAL;
|
||||
}>;
|
||||
|
||||
type ShowLowDiskSpaceBackupImportModalActionType = ReadonlyDeep<{
|
||||
type: typeof SHOW_LOW_DISK_SPACE_BACKUP_IMPORT_MODAL;
|
||||
payload: {
|
||||
bytesNeeded: number;
|
||||
};
|
||||
}>;
|
||||
|
||||
type HideLowDiskSpaceBackupImportModalActionType = ReadonlyDeep<{
|
||||
type: typeof HIDE_LOW_DISK_SPACE_BACKUP_IMPORT_MODAL;
|
||||
}>;
|
||||
|
||||
type ToggleEditNicknameAndNoteModalActionType = ReadonlyDeep<{
|
||||
type: typeof TOGGLE_EDIT_NICKNAME_AND_NOTE_MODAL;
|
||||
payload: EditNicknameAndNoteModalPropsType | null;
|
||||
@@ -489,6 +507,7 @@ export type GlobalModalsActionType = ReadonlyDeep<
|
||||
| HideBackfillFailureModalActionType
|
||||
| HideContactModalActionType
|
||||
| HideCriticalIdlePrimaryDeviceModalActionType
|
||||
| HideLowDiskSpaceBackupImportModalActionType
|
||||
| HideSendAnywayDialogActiontype
|
||||
| HideStoriesSettingsActionType
|
||||
| HideTapToViewNotAvailableModalActionType
|
||||
@@ -503,6 +522,7 @@ export type GlobalModalsActionType = ReadonlyDeep<
|
||||
| ShowContactModalActionType
|
||||
| ShowEditHistoryModalActionType
|
||||
| ShowErrorModalActionType
|
||||
| ShowLowDiskSpaceBackupImportModalActionType
|
||||
| ShowMediaPermissionsModalActionType
|
||||
| ShowSendAnywayDialogActionType
|
||||
| ShowShortcutGuideModalActionType
|
||||
@@ -548,6 +568,7 @@ export const actions = {
|
||||
hideBlockingSafetyNumberChangeDialog,
|
||||
hideContactModal,
|
||||
hideCriticalIdlePrimaryDeviceModal,
|
||||
hideLowDiskSpaceBackupImportModal,
|
||||
hideStoriesSettings,
|
||||
hideTapToViewNotAvailableModal,
|
||||
hideUserNotFoundModal,
|
||||
@@ -560,6 +581,7 @@ export const actions = {
|
||||
showEditHistoryModal,
|
||||
showErrorModal,
|
||||
showGV2MigrationDialog,
|
||||
showLowDiskSpaceBackupImportModal,
|
||||
showShareCallLinkViaSignal,
|
||||
showShortcutGuideModal,
|
||||
showStickerPackPreview,
|
||||
@@ -1179,6 +1201,23 @@ function hideCriticalIdlePrimaryDeviceModal(): ThunkAction<
|
||||
};
|
||||
}
|
||||
|
||||
function showLowDiskSpaceBackupImportModal(
|
||||
bytesNeeded: number
|
||||
): ShowLowDiskSpaceBackupImportModalActionType {
|
||||
return {
|
||||
type: SHOW_LOW_DISK_SPACE_BACKUP_IMPORT_MODAL,
|
||||
payload: {
|
||||
bytesNeeded,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function hideLowDiskSpaceBackupImportModal(): HideLowDiskSpaceBackupImportModalActionType {
|
||||
return {
|
||||
type: HIDE_LOW_DISK_SPACE_BACKUP_IMPORT_MODAL,
|
||||
};
|
||||
}
|
||||
|
||||
function toggleEditNicknameAndNoteModal(
|
||||
payload: EditNicknameAndNoteModalPropsType | null
|
||||
): ToggleEditNicknameAndNoteModalActionType {
|
||||
@@ -1303,6 +1342,7 @@ export function getEmptyState(): GlobalModalsStateType {
|
||||
isSignalConnectionsVisible: false,
|
||||
isStoriesSettingsVisible: false,
|
||||
isWhatsNewVisible: false,
|
||||
lowDiskSpaceBackupImportModal: null,
|
||||
usernameOnboardingState: UsernameOnboardingState.NeverShown,
|
||||
profileEditorHasError: false,
|
||||
profileEditorInitialEditState: undefined,
|
||||
@@ -1740,5 +1780,19 @@ export function reducer(
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === SHOW_LOW_DISK_SPACE_BACKUP_IMPORT_MODAL) {
|
||||
return {
|
||||
...state,
|
||||
lowDiskSpaceBackupImportModal: action.payload,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === HIDE_LOW_DISK_SPACE_BACKUP_IMPORT_MODAL) {
|
||||
return {
|
||||
...state,
|
||||
lowDiskSpaceBackupImportModal: null,
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -139,6 +139,7 @@ export const SmartGlobalModalContainer = memo(
|
||||
editNicknameAndNoteModalProps,
|
||||
errorModalProps,
|
||||
forwardMessagesProps,
|
||||
lowDiskSpaceBackupImportModal,
|
||||
mediaPermissionsModalProps,
|
||||
messageRequestActionsConfirmationProps,
|
||||
notePreviewModalProps,
|
||||
@@ -161,6 +162,7 @@ export const SmartGlobalModalContainer = memo(
|
||||
closeErrorModal,
|
||||
closeMediaPermissionsModal,
|
||||
hideCriticalIdlePrimaryDeviceModal,
|
||||
hideLowDiskSpaceBackupImportModal,
|
||||
hideTapToViewNotAvailableModal,
|
||||
hideUserNotFoundModal,
|
||||
hideWhatsNewModal,
|
||||
@@ -236,6 +238,8 @@ export const SmartGlobalModalContainer = memo(
|
||||
draftGifMessageSendModalProps={draftGifMessageSendModalProps}
|
||||
forwardMessagesProps={forwardMessagesProps}
|
||||
hideCriticalIdlePrimaryDeviceModal={hideCriticalIdlePrimaryDeviceModal}
|
||||
hideLowDiskSpaceBackupImportModal={hideLowDiskSpaceBackupImportModal}
|
||||
lowDiskSpaceBackupImportModal={lowDiskSpaceBackupImportModal}
|
||||
messageRequestActionsConfirmationProps={
|
||||
messageRequestActionsConfirmationProps
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import * as sinon from 'sinon';
|
||||
import { assert } from 'chai';
|
||||
import { omit } from 'lodash';
|
||||
import type { StatsFs } from 'fs';
|
||||
|
||||
import * as MIME from '../../types/MIME';
|
||||
import {
|
||||
@@ -23,6 +24,8 @@ import { type AttachmentType, AttachmentVariant } from '../../types/Attachment';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
import { AttachmentDownloadSource } from '../../sql/Interface';
|
||||
import { getAttachmentCiphertextLength } from '../../AttachmentCrypto';
|
||||
import { MEBIBYTE } from '../../types/AttachmentSize';
|
||||
import { generateAci } from '../../types/ServiceId';
|
||||
|
||||
function composeJob({
|
||||
messageId,
|
||||
@@ -66,14 +69,22 @@ describe('AttachmentDownloadManager/JobManager', () => {
|
||||
let sandbox: sinon.SinonSandbox;
|
||||
let clock: sinon.SinonFakeTimers;
|
||||
let isInCall: sinon.SinonStub;
|
||||
let onLowDiskSpaceBackupImport: sinon.SinonStub;
|
||||
let statfs: sinon.SinonStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
await DataWriter.removeAll();
|
||||
await window.storage.user.setAciAndDeviceId(generateAci(), 1);
|
||||
|
||||
sandbox = sinon.createSandbox();
|
||||
clock = sandbox.useFakeTimers();
|
||||
|
||||
isInCall = sandbox.stub().returns(false);
|
||||
onLowDiskSpaceBackupImport = sandbox
|
||||
.stub()
|
||||
.callsFake(async () =>
|
||||
window.storage.put('backupMediaDownloadPaused', true)
|
||||
);
|
||||
runJob = sandbox.stub().callsFake(async () => {
|
||||
return new Promise<{ status: 'finished' | 'retry' }>(resolve => {
|
||||
Promise.resolve().then(() => {
|
||||
@@ -81,6 +92,12 @@ describe('AttachmentDownloadManager/JobManager', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
statfs = sandbox.stub().callsFake(() =>
|
||||
Promise.resolve({
|
||||
bavail: 100_000_000_000,
|
||||
bsize: 100,
|
||||
} as StatsFs)
|
||||
);
|
||||
|
||||
downloadManager = new AttachmentDownloadManager({
|
||||
...AttachmentDownloadManager.defaultParams,
|
||||
@@ -95,6 +112,8 @@ describe('AttachmentDownloadManager/JobManager', () => {
|
||||
maxBackoffTime: 10 * MINUTE,
|
||||
},
|
||||
}),
|
||||
onLowDiskSpaceBackupImport,
|
||||
statfs,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -294,6 +313,39 @@ describe('AttachmentDownloadManager/JobManager', () => {
|
||||
assert.strictEqual(runJob.callCount, 5);
|
||||
});
|
||||
|
||||
it('triggers onLowDiskSpace for backup import jobs', async () => {
|
||||
const jobs = await addJobs(1, idx => ({
|
||||
source: AttachmentDownloadSource.BACKUP_IMPORT,
|
||||
digest: `digestFor${idx}`,
|
||||
attachment: {
|
||||
contentType: MIME.IMAGE_JPEG,
|
||||
size: 128,
|
||||
digest: `digestFor${idx}`,
|
||||
backupLocator: {
|
||||
mediaName: 'medianame',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
statfs.callsFake(() => Promise.resolve({ bavail: 0, bsize: 8 }));
|
||||
|
||||
await downloadManager?.start();
|
||||
await advanceTime(2 * MINUTE);
|
||||
assert.strictEqual(runJob.callCount, 0);
|
||||
assert.strictEqual(onLowDiskSpaceBackupImport.callCount, 1);
|
||||
assert.isTrue(window.storage.get('backupMediaDownloadPaused'));
|
||||
|
||||
statfs.callsFake(() =>
|
||||
Promise.resolve({ bavail: 100_000_000_000, bsize: 8 })
|
||||
);
|
||||
|
||||
const job0Attempts = getPromisesForAttempts(jobs[0], 1)[0];
|
||||
await window.storage.put('backupMediaDownloadPaused', false);
|
||||
await advanceTime(2 * MINUTE);
|
||||
assert.strictEqual(runJob.callCount, 1);
|
||||
await job0Attempts.started;
|
||||
});
|
||||
|
||||
it('handles retries for failed', async () => {
|
||||
const jobs = await addJobs(2);
|
||||
const job0Attempts = getPromisesForAttempts(jobs[0], 1);
|
||||
@@ -478,6 +530,8 @@ describe('AttachmentDownloadManager/runDownloadAttachmentJob', () => {
|
||||
job,
|
||||
isForCurrentlyVisibleMessage: true,
|
||||
abortSignal: abortController.signal,
|
||||
maxAttachmentSizeInKib: 100 * MEBIBYTE,
|
||||
maxTextAttachmentSizeInKib: 2 * MEBIBYTE,
|
||||
dependencies: {
|
||||
deleteDownloadData,
|
||||
downloadAttachment,
|
||||
@@ -510,6 +564,8 @@ describe('AttachmentDownloadManager/runDownloadAttachmentJob', () => {
|
||||
job,
|
||||
isForCurrentlyVisibleMessage: true,
|
||||
abortSignal: abortController.signal,
|
||||
maxAttachmentSizeInKib: 100 * MEBIBYTE,
|
||||
maxTextAttachmentSizeInKib: 2 * MEBIBYTE,
|
||||
dependencies: {
|
||||
deleteDownloadData,
|
||||
downloadAttachment,
|
||||
@@ -561,6 +617,8 @@ describe('AttachmentDownloadManager/runDownloadAttachmentJob', () => {
|
||||
job,
|
||||
isForCurrentlyVisibleMessage: true,
|
||||
abortSignal: abortController.signal,
|
||||
maxAttachmentSizeInKib: 100 * MEBIBYTE,
|
||||
maxTextAttachmentSizeInKib: 2 * MEBIBYTE,
|
||||
dependencies: {
|
||||
deleteDownloadData,
|
||||
downloadAttachment,
|
||||
@@ -598,6 +656,8 @@ describe('AttachmentDownloadManager/runDownloadAttachmentJob', () => {
|
||||
job,
|
||||
isForCurrentlyVisibleMessage: true,
|
||||
abortSignal: abortController.signal,
|
||||
maxAttachmentSizeInKib: 100 * MEBIBYTE,
|
||||
maxTextAttachmentSizeInKib: 2 * MEBIBYTE,
|
||||
dependencies: {
|
||||
deleteDownloadData,
|
||||
downloadAttachment,
|
||||
@@ -639,6 +699,8 @@ describe('AttachmentDownloadManager/runDownloadAttachmentJob', () => {
|
||||
job,
|
||||
isForCurrentlyVisibleMessage: false,
|
||||
abortSignal: abortController.signal,
|
||||
maxAttachmentSizeInKib: 100 * MEBIBYTE,
|
||||
maxTextAttachmentSizeInKib: 2 * MEBIBYTE,
|
||||
dependencies: {
|
||||
deleteDownloadData,
|
||||
downloadAttachment,
|
||||
@@ -681,6 +743,8 @@ describe('AttachmentDownloadManager/runDownloadAttachmentJob', () => {
|
||||
job,
|
||||
isForCurrentlyVisibleMessage: false,
|
||||
abortSignal: abortController.signal,
|
||||
maxAttachmentSizeInKib: 100 * MEBIBYTE,
|
||||
maxTextAttachmentSizeInKib: 2 * MEBIBYTE,
|
||||
dependencies: {
|
||||
deleteDownloadData,
|
||||
downloadAttachment,
|
||||
@@ -730,6 +794,8 @@ describe('AttachmentDownloadManager/runDownloadAttachmentJob', () => {
|
||||
job,
|
||||
isForCurrentlyVisibleMessage: false,
|
||||
abortSignal: abortController.signal,
|
||||
maxAttachmentSizeInKib: 100 * MEBIBYTE,
|
||||
maxTextAttachmentSizeInKib: 2 * MEBIBYTE,
|
||||
dependencies: {
|
||||
deleteDownloadData,
|
||||
downloadAttachment,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { parseIntOrThrow } from '../util/parseIntOrThrow';
|
||||
import type * as RemoteConfig from '../RemoteConfig';
|
||||
|
||||
export const KIBIBYTE = 1024;
|
||||
const MEBIBYTE = 1024 * 1024;
|
||||
export const MEBIBYTE = 1024 * 1024;
|
||||
const DEFAULT_MAX = 100 * MEBIBYTE;
|
||||
|
||||
export const getMaximumOutgoingAttachmentSizeInKb = (
|
||||
|
||||
Reference in New Issue
Block a user