Use streams to download attachments directly to disk
Co-authored-by: trevor-signal <131492920+trevor-signal@users.noreply.github.com>
This commit is contained in:
parent
2da49456c6
commit
99b2bc304e
48 changed files with 2297 additions and 356 deletions
|
@ -37,6 +37,8 @@ const MIN_HEIGHT = 50;
|
|||
|
||||
// Used for display
|
||||
|
||||
export class AttachmentSizeError extends Error {}
|
||||
|
||||
export type AttachmentType = {
|
||||
error?: boolean;
|
||||
blurHash?: string;
|
||||
|
@ -75,6 +77,7 @@ export type AttachmentType = {
|
|||
key?: string;
|
||||
data?: Uint8Array;
|
||||
textAttachment?: TextAttachmentType;
|
||||
wasTooBig?: boolean;
|
||||
|
||||
/** Legacy field. Used only for downloading old attachments */
|
||||
id?: number;
|
||||
|
@ -1008,9 +1011,9 @@ export const defaultBlurHash = (theme: ThemeType = ThemeType.light): string => {
|
|||
};
|
||||
|
||||
export const canBeDownloaded = (
|
||||
attachment: Pick<AttachmentType, 'key' | 'digest'>
|
||||
attachment: Pick<AttachmentType, 'digest' | 'key' | 'wasTooBig'>
|
||||
): boolean => {
|
||||
return Boolean(attachment.key && attachment.digest);
|
||||
return Boolean(attachment.digest && attachment.key && !attachment.wasTooBig);
|
||||
};
|
||||
|
||||
export function getAttachmentSignature(attachment: AttachmentType): string {
|
||||
|
|
|
@ -9,14 +9,14 @@ export const KIBIBYTE = 1024;
|
|||
const MEBIBYTE = 1024 * 1024;
|
||||
const DEFAULT_MAX = 100 * MEBIBYTE;
|
||||
|
||||
export const getMaximumAttachmentSizeInKb = (
|
||||
export const getMaximumOutgoingAttachmentSizeInKb = (
|
||||
getValue: typeof RemoteConfig.getValue
|
||||
): number => {
|
||||
try {
|
||||
return (
|
||||
parseIntOrThrow(
|
||||
getValue('global.attachments.maxBytes'),
|
||||
'preProcessAttachment/maxAttachmentSize'
|
||||
'getMaximumOutgoingAttachmentSizeInKb'
|
||||
) / KIBIBYTE
|
||||
);
|
||||
} catch (error) {
|
||||
|
@ -27,6 +27,22 @@ export const getMaximumAttachmentSizeInKb = (
|
|||
}
|
||||
};
|
||||
|
||||
export const getMaximumIncomingAttachmentSizeInKb = (
|
||||
getValue: typeof RemoteConfig.getValue
|
||||
): number => {
|
||||
try {
|
||||
return (
|
||||
parseIntOrThrow(
|
||||
getValue('global.attachments.maxReceiveBytes'),
|
||||
'getMaximumIncomingAttachmentSizeInKb'
|
||||
) / KIBIBYTE
|
||||
);
|
||||
} catch (_error) {
|
||||
// TODO: DESKTOP-5913. We're not gonna log until the new flag is fully deployed
|
||||
return getMaximumOutgoingAttachmentSizeInKb(getValue) * 1.25;
|
||||
}
|
||||
};
|
||||
|
||||
export function getRenderDetailsForLimit(limitKb: number): {
|
||||
limit: number;
|
||||
units: string;
|
||||
|
|
|
@ -34,6 +34,12 @@ export const GroupAvatarIcons = [
|
|||
'surfboard',
|
||||
] as const;
|
||||
|
||||
export type ContactAvatarType = {
|
||||
path: string;
|
||||
url?: string;
|
||||
hash?: string;
|
||||
};
|
||||
|
||||
type GroupAvatarIconType = typeof GroupAvatarIcons[number];
|
||||
|
||||
type PersonalAvatarIconType = typeof PersonalAvatarIcons[number];
|
||||
|
|
|
@ -2,33 +2,87 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ConversationAttributesType } from '../model-types.d';
|
||||
import type { ContactAvatarType } from './Avatar';
|
||||
import { computeHash } from '../Crypto';
|
||||
|
||||
export type BuildAvatarUpdaterOptions = Readonly<{
|
||||
data?: Uint8Array;
|
||||
newAvatar?: ContactAvatarType;
|
||||
deleteAttachmentData: (path: string) => Promise<void>;
|
||||
doesAttachmentExist: (path: string) => Promise<boolean>;
|
||||
writeNewAttachmentData: (data: Uint8Array) => Promise<string>;
|
||||
}>;
|
||||
|
||||
// This function is ready to handle raw avatar data as well as an avatar which has
|
||||
// already been downloaded to disk.
|
||||
// Scenarios that go to disk today:
|
||||
// - During a contact sync (see ContactsParser.ts)
|
||||
// Scenarios that stay in memory today:
|
||||
// - models/Conversations/setProfileAvatar
|
||||
function buildAvatarUpdater({ field }: { field: 'avatar' | 'profileAvatar' }) {
|
||||
return async (
|
||||
conversation: Readonly<ConversationAttributesType>,
|
||||
data: Uint8Array,
|
||||
{
|
||||
data,
|
||||
newAvatar,
|
||||
deleteAttachmentData,
|
||||
doesAttachmentExist,
|
||||
writeNewAttachmentData,
|
||||
}: BuildAvatarUpdaterOptions
|
||||
): Promise<ConversationAttributesType> => {
|
||||
if (!conversation) {
|
||||
if (!conversation || (!data && !newAvatar)) {
|
||||
return conversation;
|
||||
}
|
||||
|
||||
const avatar = conversation[field];
|
||||
const oldAvatar = conversation[field];
|
||||
const newHash = data ? computeHash(data) : undefined;
|
||||
|
||||
const newHash = computeHash(data);
|
||||
if (!oldAvatar || !oldAvatar.hash) {
|
||||
if (newAvatar) {
|
||||
return {
|
||||
...conversation,
|
||||
[field]: newAvatar,
|
||||
};
|
||||
}
|
||||
if (data) {
|
||||
return {
|
||||
...conversation,
|
||||
[field]: {
|
||||
hash: newHash,
|
||||
path: await writeNewAttachmentData(data),
|
||||
},
|
||||
};
|
||||
}
|
||||
throw new Error('buildAvatarUpdater: neither newAvatar or newData');
|
||||
}
|
||||
|
||||
if (!avatar || !avatar.hash) {
|
||||
const { hash, path } = oldAvatar;
|
||||
const exists = await doesAttachmentExist(path);
|
||||
if (!exists) {
|
||||
window.SignalContext.log.warn(
|
||||
`Conversation.buildAvatarUpdater: attachment ${path} did not exist`
|
||||
);
|
||||
}
|
||||
|
||||
if (exists) {
|
||||
if (newAvatar && hash && hash === newAvatar.hash) {
|
||||
await deleteAttachmentData(newAvatar.path);
|
||||
return conversation;
|
||||
}
|
||||
if (data && hash && hash === newHash) {
|
||||
return conversation;
|
||||
}
|
||||
}
|
||||
|
||||
await deleteAttachmentData(path);
|
||||
|
||||
if (newAvatar) {
|
||||
return {
|
||||
...conversation,
|
||||
[field]: newAvatar,
|
||||
};
|
||||
}
|
||||
if (data) {
|
||||
return {
|
||||
...conversation,
|
||||
[field]: {
|
||||
|
@ -38,27 +92,7 @@ function buildAvatarUpdater({ field }: { field: 'avatar' | 'profileAvatar' }) {
|
|||
};
|
||||
}
|
||||
|
||||
const { hash, path } = avatar;
|
||||
const exists = await doesAttachmentExist(path);
|
||||
if (!exists) {
|
||||
window.SignalContext.log.warn(
|
||||
`Conversation.buildAvatarUpdater: attachment ${path} did not exist`
|
||||
);
|
||||
}
|
||||
|
||||
if (exists && hash === newHash) {
|
||||
return conversation;
|
||||
}
|
||||
|
||||
await deleteAttachmentData(path);
|
||||
|
||||
return {
|
||||
...conversation,
|
||||
[field]: {
|
||||
hash: newHash,
|
||||
path: await writeNewAttachmentData(data),
|
||||
},
|
||||
};
|
||||
throw new Error('buildAvatarUpdater: neither newAvatar or newData');
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -593,10 +593,18 @@ export const processNewAttachment = async (
|
|||
isIncoming: true,
|
||||
}
|
||||
);
|
||||
const onDiskAttachment = await migrateDataToFileSystem(rotatedAttachment, {
|
||||
writeNewAttachmentData,
|
||||
logger,
|
||||
});
|
||||
|
||||
let onDiskAttachment = rotatedAttachment;
|
||||
|
||||
// If we rotated the attachment, then `data` will be the actual bytes of the attachment,
|
||||
// in memory. We want that updated attachment to go back to disk.
|
||||
if (rotatedAttachment.data) {
|
||||
onDiskAttachment = await migrateDataToFileSystem(rotatedAttachment, {
|
||||
writeNewAttachmentData,
|
||||
logger,
|
||||
});
|
||||
}
|
||||
|
||||
const finalAttachment = await captureDimensionsAndScreenshot(
|
||||
onDiskAttachment,
|
||||
{
|
||||
|
|
|
@ -11,7 +11,7 @@ import { makeLookup } from '../util/makeLookup';
|
|||
import { maybeParseUrl } from '../util/url';
|
||||
import * as Bytes from '../Bytes';
|
||||
import * as Errors from './errors';
|
||||
import { deriveStickerPackKey, decryptAttachment } from '../Crypto';
|
||||
import { deriveStickerPackKey, decryptAttachmentV1 } from '../Crypto';
|
||||
import { IMAGE_WEBP } from './MIME';
|
||||
import type { MIMEType } from './MIME';
|
||||
import { sniffImageMimeType } from '../util/sniffImageMimeType';
|
||||
|
@ -310,7 +310,10 @@ function getReduxStickerActions() {
|
|||
function decryptSticker(packKey: string, ciphertext: Uint8Array): Uint8Array {
|
||||
const binaryKey = Bytes.fromBase64(packKey);
|
||||
const derivedKey = deriveStickerPackKey(binaryKey);
|
||||
const plaintext = decryptAttachment(ciphertext, derivedKey);
|
||||
|
||||
// Note this download and decrypt in memory is okay because these files are maximum
|
||||
// 300kb, enforced by the server.
|
||||
const plaintext = decryptAttachmentV1(ciphertext, derivedKey);
|
||||
|
||||
return plaintext;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue