Support thumbnail export & import during backup of visual attachments
This commit is contained in:
parent
451ee56c92
commit
61548061b8
30 changed files with 1326 additions and 327 deletions
|
@ -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 {
|
||||
|
|
|
@ -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(),
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue