mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-05-08 08:58:38 +01:00
New attachment storage system
This commit is contained in:
+65
-35
@@ -30,6 +30,7 @@ import { strictAssert } from '../util/assert';
|
||||
import type { SignalService as Proto } from '../protobuf';
|
||||
import { isMoreRecentThan } from '../util/timestamp';
|
||||
import { DAY } from '../util/durations';
|
||||
import { getLocalAttachmentUrl } from '../util/getLocalAttachmentUrl';
|
||||
|
||||
const MAX_WIDTH = 300;
|
||||
const MAX_HEIGHT = MAX_WIDTH * 1.5;
|
||||
@@ -40,6 +41,13 @@ const MIN_HEIGHT = 50;
|
||||
|
||||
export class AttachmentSizeError extends Error {}
|
||||
|
||||
type ScreenshotType = Omit<AttachmentType, 'size'> & {
|
||||
height: number;
|
||||
width: number;
|
||||
path: string;
|
||||
size?: number;
|
||||
};
|
||||
|
||||
export type AttachmentType = {
|
||||
error?: boolean;
|
||||
blurHash?: string;
|
||||
@@ -60,15 +68,9 @@ export type AttachmentType = {
|
||||
width?: number;
|
||||
height?: number;
|
||||
path?: string;
|
||||
screenshot?: {
|
||||
height: number;
|
||||
width: number;
|
||||
url?: string;
|
||||
contentType: MIME.MIMEType;
|
||||
path: string;
|
||||
data?: Uint8Array;
|
||||
};
|
||||
screenshot?: ScreenshotType;
|
||||
screenshotData?: Uint8Array;
|
||||
// Legacy Draft
|
||||
screenshotPath?: string;
|
||||
flags?: number;
|
||||
thumbnail?: ThumbnailType;
|
||||
@@ -90,6 +92,10 @@ export type AttachmentType = {
|
||||
cdnNumber?: number;
|
||||
};
|
||||
|
||||
// See app/attachment_channel.ts
|
||||
version?: 1 | 2;
|
||||
localKey?: string; // AES + MAC
|
||||
|
||||
/** Legacy field. Used only for downloading old attachments */
|
||||
id?: number;
|
||||
|
||||
@@ -97,6 +103,24 @@ export type AttachmentType = {
|
||||
schemaVersion?: number;
|
||||
};
|
||||
|
||||
export type LocalAttachmentV2Type = Readonly<{
|
||||
version: 2;
|
||||
path: string;
|
||||
localKey: string;
|
||||
plaintextHash: string;
|
||||
size: number;
|
||||
}>;
|
||||
|
||||
export type AddressableAttachmentType = Readonly<{
|
||||
version?: 1 | 2;
|
||||
path: string;
|
||||
localKey?: string;
|
||||
size?: number;
|
||||
|
||||
// In-memory data, for outgoing attachments that are not saved to disk.
|
||||
data?: Uint8Array;
|
||||
}>;
|
||||
|
||||
export type UploadedAttachmentType = Proto.IAttachmentPointer &
|
||||
Readonly<{
|
||||
// Required fields
|
||||
@@ -138,10 +162,6 @@ export type TextAttachmentType = {
|
||||
color?: number | null;
|
||||
};
|
||||
|
||||
export type DownloadedAttachmentType = AttachmentType & {
|
||||
data: Uint8Array;
|
||||
};
|
||||
|
||||
export type BaseAttachmentDraftType = {
|
||||
blurHash?: string;
|
||||
contentType: MIME.MIMEType;
|
||||
@@ -174,6 +194,8 @@ export type InMemoryAttachmentDraftType =
|
||||
export type AttachmentDraftType =
|
||||
| ({
|
||||
url?: string;
|
||||
screenshot?: ScreenshotType;
|
||||
// Legacy field
|
||||
screenshotPath?: string;
|
||||
pending: false;
|
||||
// Old draft attachments may have a caption, though they are no longer editable
|
||||
@@ -184,6 +206,8 @@ export type AttachmentDraftType =
|
||||
width?: number;
|
||||
height?: number;
|
||||
clientUuid: string;
|
||||
version?: 2;
|
||||
localKey?: string;
|
||||
} & BaseAttachmentDraftType)
|
||||
| {
|
||||
clientUuid: string;
|
||||
@@ -318,11 +342,13 @@ export function hasData(attachment: AttachmentType): boolean {
|
||||
}
|
||||
|
||||
export function loadData(
|
||||
readAttachmentData: (path: string) => Promise<Uint8Array>
|
||||
readAttachmentV2Data: (
|
||||
attachment: Partial<AddressableAttachmentType>
|
||||
) => Promise<Uint8Array>
|
||||
): (
|
||||
attachment: Pick<AttachmentType, 'data' | 'path'>
|
||||
attachment: Partial<AttachmentType>
|
||||
) => Promise<AttachmentWithHydratedData> {
|
||||
if (!isFunction(readAttachmentData)) {
|
||||
if (!isFunction(readAttachmentV2Data)) {
|
||||
throw new TypeError("'readAttachmentData' must be a function");
|
||||
}
|
||||
|
||||
@@ -340,7 +366,7 @@ export function loadData(
|
||||
throw new TypeError("'attachment.path' is required");
|
||||
}
|
||||
|
||||
const data = await readAttachmentData(attachment.path);
|
||||
const data = await readAttachmentV2Data(attachment);
|
||||
return { ...attachment, data, size: data.byteLength };
|
||||
};
|
||||
}
|
||||
@@ -378,8 +404,9 @@ const THUMBNAIL_CONTENT_TYPE = MIME.IMAGE_PNG;
|
||||
export async function captureDimensionsAndScreenshot(
|
||||
attachment: AttachmentType,
|
||||
params: {
|
||||
writeNewAttachmentData: (data: Uint8Array) => Promise<string>;
|
||||
getAbsoluteAttachmentPath: (path: string) => string;
|
||||
writeNewAttachmentData: (
|
||||
data: Uint8Array
|
||||
) => Promise<LocalAttachmentV2Type>;
|
||||
makeObjectUrl: (
|
||||
data: Uint8Array | ArrayBuffer,
|
||||
contentType: MIME.MIMEType
|
||||
@@ -410,7 +437,6 @@ export async function captureDimensionsAndScreenshot(
|
||||
|
||||
const {
|
||||
writeNewAttachmentData,
|
||||
getAbsoluteAttachmentPath,
|
||||
makeObjectUrl,
|
||||
revokeObjectUrl,
|
||||
getImageDimensions: getImageDimensionsFromURL,
|
||||
@@ -431,24 +457,24 @@ export async function captureDimensionsAndScreenshot(
|
||||
return attachment;
|
||||
}
|
||||
|
||||
const absolutePath = getAbsoluteAttachmentPath(attachment.path);
|
||||
const localUrl = getLocalAttachmentUrl(attachment);
|
||||
|
||||
if (GoogleChrome.isImageTypeSupported(contentType)) {
|
||||
try {
|
||||
const { width, height } = await getImageDimensionsFromURL({
|
||||
objectUrl: absolutePath,
|
||||
objectUrl: localUrl,
|
||||
logger,
|
||||
});
|
||||
const thumbnailBuffer = await blobToArrayBuffer(
|
||||
await makeImageThumbnail({
|
||||
size: THUMBNAIL_SIZE,
|
||||
objectUrl: absolutePath,
|
||||
objectUrl: localUrl,
|
||||
contentType: THUMBNAIL_CONTENT_TYPE,
|
||||
logger,
|
||||
})
|
||||
);
|
||||
|
||||
const thumbnailPath = await writeNewAttachmentData(
|
||||
const thumbnail = await writeNewAttachmentData(
|
||||
new Uint8Array(thumbnailBuffer)
|
||||
);
|
||||
return {
|
||||
@@ -456,11 +482,10 @@ export async function captureDimensionsAndScreenshot(
|
||||
width,
|
||||
height,
|
||||
thumbnail: {
|
||||
path: thumbnailPath,
|
||||
...thumbnail,
|
||||
contentType: THUMBNAIL_CONTENT_TYPE,
|
||||
width: THUMBNAIL_SIZE,
|
||||
height: THUMBNAIL_SIZE,
|
||||
size: thumbnailBuffer.byteLength,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -477,7 +502,7 @@ export async function captureDimensionsAndScreenshot(
|
||||
try {
|
||||
const screenshotBuffer = await blobToArrayBuffer(
|
||||
await makeVideoScreenshot({
|
||||
objectUrl: absolutePath,
|
||||
objectUrl: localUrl,
|
||||
contentType: THUMBNAIL_CONTENT_TYPE,
|
||||
logger,
|
||||
})
|
||||
@@ -490,7 +515,7 @@ export async function captureDimensionsAndScreenshot(
|
||||
objectUrl: screenshotObjectUrl,
|
||||
logger,
|
||||
});
|
||||
const screenshotPath = await writeNewAttachmentData(
|
||||
const screenshot = await writeNewAttachmentData(
|
||||
new Uint8Array(screenshotBuffer)
|
||||
);
|
||||
|
||||
@@ -503,24 +528,23 @@ export async function captureDimensionsAndScreenshot(
|
||||
})
|
||||
);
|
||||
|
||||
const thumbnailPath = await writeNewAttachmentData(
|
||||
const thumbnail = await writeNewAttachmentData(
|
||||
new Uint8Array(thumbnailBuffer)
|
||||
);
|
||||
|
||||
return {
|
||||
...attachment,
|
||||
screenshot: {
|
||||
...screenshot,
|
||||
contentType: THUMBNAIL_CONTENT_TYPE,
|
||||
path: screenshotPath,
|
||||
width,
|
||||
height,
|
||||
},
|
||||
thumbnail: {
|
||||
path: thumbnailPath,
|
||||
...thumbnail,
|
||||
contentType: THUMBNAIL_CONTENT_TYPE,
|
||||
width: THUMBNAIL_SIZE,
|
||||
height: THUMBNAIL_SIZE,
|
||||
size: thumbnailBuffer.byteLength,
|
||||
},
|
||||
width,
|
||||
height,
|
||||
@@ -663,14 +687,18 @@ export function hasImage(attachments?: ReadonlyArray<AttachmentType>): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
export function isVideo(attachments?: ReadonlyArray<AttachmentType>): boolean {
|
||||
export function isVideo(
|
||||
attachments?: ReadonlyArray<Pick<AttachmentType, 'contentType'>>
|
||||
): boolean {
|
||||
if (!attachments || attachments.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return isVideoAttachment(attachments[0]);
|
||||
}
|
||||
|
||||
export function isVideoAttachment(attachment?: AttachmentType): boolean {
|
||||
export function isVideoAttachment(
|
||||
attachment?: Pick<AttachmentType, 'contentType'>
|
||||
): boolean {
|
||||
if (!attachment || !attachment.contentType) {
|
||||
return false;
|
||||
}
|
||||
@@ -914,7 +942,9 @@ export const save = async ({
|
||||
}: {
|
||||
attachment: AttachmentType;
|
||||
index?: number;
|
||||
readAttachmentData: (relativePath: string) => Promise<Uint8Array>;
|
||||
readAttachmentData: (
|
||||
attachment: Partial<AddressableAttachmentType>
|
||||
) => Promise<Uint8Array>;
|
||||
saveAttachmentToDisk: (options: {
|
||||
data: Uint8Array;
|
||||
name: string;
|
||||
@@ -923,7 +953,7 @@ export const save = async ({
|
||||
}): Promise<string | null> => {
|
||||
let data: Uint8Array;
|
||||
if (attachment.path) {
|
||||
data = await readAttachmentData(attachment.path);
|
||||
data = await readAttachmentData(attachment);
|
||||
} else if (attachment.data) {
|
||||
data = attachment.data;
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user