New attachment storage system

This commit is contained in:
Fedor Indutny
2024-07-11 12:44:09 -07:00
committed by GitHub
parent 273e1ccb15
commit 28664a606f
161 changed files with 2418 additions and 1562 deletions
+65 -35
View File
@@ -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 {