Support thumbnail export & import during backup of visual attachments

This commit is contained in:
trevor-signal 2024-07-16 16:39:56 -04:00 committed by GitHub
parent 451ee56c92
commit 61548061b8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 1326 additions and 327 deletions

View file

@ -95,6 +95,10 @@ export type AttachmentType = {
// See app/attachment_channel.ts
version?: 1 | 2;
localKey?: string; // AES + MAC
thumbnailFromBackup?: Pick<
AttachmentType,
'path' | 'version' | 'plaintextHash'
>;
/** Legacy field. Used only for downloading old attachments */
id?: number;
@ -121,6 +125,12 @@ export type AddressableAttachmentType = Readonly<{
data?: Uint8Array;
}>;
export type AttachmentForUIType = AttachmentType & {
thumbnailFromBackup?: {
url?: string;
};
};
export type UploadedAttachmentType = Proto.IAttachmentPointer &
Readonly<{
// Required fields
@ -225,6 +235,11 @@ export type ThumbnailType = AttachmentType & {
copied?: boolean;
};
export enum AttachmentVariant {
Default = 'Default',
ThumbnailFromBackup = 'thumbnailFromBackup',
}
// // Incoming message attachment fields
// {
// id: string
@ -383,7 +398,8 @@ export function deleteData(
throw new TypeError('deleteData: attachment is not valid');
}
const { path, thumbnail, screenshot } = attachment;
const { path, thumbnail, screenshot, thumbnailFromBackup } = attachment;
if (isString(path)) {
await deleteOnDisk(path);
}
@ -395,6 +411,10 @@ export function deleteData(
if (screenshot && isString(screenshot.path)) {
await deleteOnDisk(screenshot.path);
}
if (thumbnailFromBackup && isString(thumbnailFromBackup.path)) {
await deleteOnDisk(thumbnailFromBackup.path);
}
};
}
@ -629,7 +649,7 @@ export function canDisplayImage(
}
export function getThumbnailUrl(
attachment: AttachmentType
attachment: AttachmentForUIType
): string | undefined {
if (attachment.thumbnail) {
return attachment.thumbnail.url;
@ -638,7 +658,7 @@ export function getThumbnailUrl(
return getUrl(attachment);
}
export function getUrl(attachment: AttachmentType): string | undefined {
export function getUrl(attachment: AttachmentForUIType): string | undefined {
if (attachment.screenshot) {
return attachment.screenshot.url;
}
@ -647,7 +667,7 @@ export function getUrl(attachment: AttachmentType): string | undefined {
return undefined;
}
return attachment.url;
return attachment.url ?? attachment.thumbnailFromBackup?.url;
}
export function isImage(attachments?: ReadonlyArray<AttachmentType>): boolean {

View file

@ -11,7 +11,7 @@ export type CoreAttachmentBackupJobType =
| StandardAttachmentBackupJobType
| ThumbnailAttachmentBackupJobType;
type StandardAttachmentBackupJobType = {
export type StandardAttachmentBackupJobType = {
type: 'standard';
mediaName: string;
receivedAt: number;
@ -27,20 +27,21 @@ type StandardAttachmentBackupJobType = {
uploadTimestamp?: number;
};
size: number;
version?: 2;
version?: 1 | 2;
localKey?: string;
};
};
type ThumbnailAttachmentBackupJobType = {
export type ThumbnailAttachmentBackupJobType = {
type: 'thumbnail';
mediaName: string;
receivedAt: number;
data: {
fullsizePath: string | null;
fullsizeSize: number;
contentType: MIMEType;
keys: string;
version?: 1 | 2;
localKey?: string;
};
};
@ -60,7 +61,7 @@ const standardBackupJobDataSchema = z.object({
uploadTimestamp: z.number().optional(),
})
.optional(),
version: z.literal(2).optional(),
version: z.union([z.literal(1), z.literal(2)]).optional(),
localKey: z.string().optional(),
}),
});
@ -69,8 +70,10 @@ const thumbnailBackupJobDataSchema = z.object({
type: z.literal('thumbnail'),
data: z.object({
fullsizePath: z.string(),
fullsizeSize: z.number(),
contentType: MIMETypeSchema,
keys: z.string(),
version: z.union([z.literal(1), z.literal(2)]).optional(),
localKey: z.string().optional(),
}),
});

View file

@ -12,9 +12,12 @@ import { canvasToBlob } from '../util/canvasToBlob';
import { KIBIBYTE } from './AttachmentSize';
import { explodePromise } from '../util/explodePromise';
import { SECOND } from '../util/durations';
import * as logging from '../logging/log';
export { blobToArrayBuffer };
export const MAX_BACKUP_THUMBNAIL_SIZE = 8 * KIBIBYTE;
export type GetImageDimensionsOptionsType = Readonly<{
objectUrl: string;
logger: Pick<LoggerType, 'error'>;
@ -107,7 +110,6 @@ export type MakeImageThumbnailForBackupOptionsType = Readonly<{
maxDimension?: number;
maxSize?: number;
objectUrl: string;
logger: LoggerType;
}>;
// 0.7 quality seems to result in a good result in 1 interation for most images
@ -122,11 +124,10 @@ export type CreatedThumbnailType = {
mimeType: MIMEType;
};
export function makeImageThumbnailForBackup({
export async function makeImageThumbnailForBackup({
maxDimension = 256,
maxSize = 8 * KIBIBYTE,
maxSize = MAX_BACKUP_THUMBNAIL_SIZE,
objectUrl,
logger,
}: MakeImageThumbnailForBackupOptionsType): Promise<CreatedThumbnailType> {
return new Promise((resolve, reject) => {
const image = document.createElement('img');
@ -174,7 +175,7 @@ export function makeImageThumbnailForBackup({
const duration = (performance.now() - start).toFixed(1);
const logMethod = blob.size > maxSize ? logger.warn : logger.info;
const logMethod = blob.size > maxSize ? logging.warn : logging.info;
const sizeInKiB = blob.size / KIBIBYTE;
logMethod(
'makeImageThumbnail: generated thumbnail of dimensions: ' +
@ -196,7 +197,7 @@ export function makeImageThumbnailForBackup({
});
image.addEventListener('error', error => {
logger.error('makeImageThumbnail error', toLogFormat(error));
logging.error('makeImageThumbnail error', toLogFormat(error));
reject(error);
});
@ -207,7 +208,6 @@ export function makeImageThumbnailForBackup({
export type MakeVideoScreenshotOptionsType = Readonly<{
objectUrl: string;
contentType?: MIMEType;
logger: Pick<LoggerType, 'error'>;
}>;
const MAKE_VIDEO_SCREENSHOT_TIMEOUT = 30 * SECOND;
@ -228,7 +228,6 @@ function captureScreenshot(
export async function makeVideoScreenshot({
objectUrl,
contentType = IMAGE_PNG,
logger,
}: MakeVideoScreenshotOptionsType): Promise<Blob> {
const signal = AbortSignal.timeout(MAKE_VIDEO_SCREENSHOT_TIMEOUT);
const video = document.createElement('video');
@ -256,7 +255,7 @@ export async function makeVideoScreenshot({
await videoLoadedAndSeeked;
return await captureScreenshot(video, contentType);
} catch (error) {
logger.error('makeVideoScreenshot error:', toLogFormat(error));
logging.error('makeVideoScreenshot error:', toLogFormat(error));
throw error;
} finally {
// hard reset the video element so it doesn't keep loading