Skip to content
Merged
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 36 additions & 6 deletions apps/meteor/server/lib/media/file-upload/lib/FileUpload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { Match } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
import { Cookies } from 'meteor/ostrio:cookies';
import type { ClientSession, OptionalId } from 'mongodb';
import sharp from 'sharp';
import sharp, { type Sharp } from 'sharp';
import type { WritableStreamBuffer } from 'stream-buffers';
import streamBuffers from 'stream-buffers';

Expand All @@ -40,6 +40,28 @@ import { validateAndDecodeJWT, generateJWT } from '../../../utils/lib/JWTHelper'
import { fileUploadIsValidContentType } from '../../../utils/restrictions';

const cookie = new Cookies();

// Bounds per-frame GIF decode cost; sharp's limitInputPixels alone doesn't catch a huge frame count with tiny per-frame dimensions.
const MAX_ANIMATED_THUMBNAIL_PAGES = 100;

// FileUpload.getBuffer never settles if the underlying store read errors (e.g. a network blip on S3/GCS/WebDAV),
// which would otherwise hang the sendFileMessage method call indefinitely.
const GET_BUFFER_TIMEOUT_MS = 30_000;

async function getBufferWithTimeout(file: IUpload): Promise<Buffer> {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
let timer: ReturnType<typeof setTimeout>;
try {
return await Promise.race([
FileUpload.getBuffer(file),
new Promise<never>((_, reject) => {
timer = setTimeout(() => reject(new Error('Timed out reading file buffer for thumbnail generation')), GET_BUFFER_TIMEOUT_MS);
}),
]);
Comment thread
julio-rocketchat marked this conversation as resolved.
Resolved
} finally {
clearTimeout(timer!);
}
}
Comment thread
julio-rocketchat marked this conversation as resolved.
Resolved

let maxFileSize = 0;

settings.watch('FileUpload_MaxFileSize', async (value: string) => {
Expand Down Expand Up @@ -326,22 +348,30 @@ export const FileUpload = {

file = FileUpload.addExtensionTo(file);
const store = FileUpload.getStore('Uploads');
const image = await store._store.getReadStream(file._id, file);

let transformer = sharp().resize({ width, height, fit: 'inside' });
let transformer: Sharp;
if (file.type === 'image/gif') {
Comment thread
julio-rocketchat marked this conversation as resolved.
Resolved
const buffer = await getBufferWithTimeout(file);
const { pages: totalPages } = await sharp(buffer).metadata();
const pages = Math.min(totalPages ?? 1, MAX_ANIMATED_THUMBNAIL_PAGES);
Comment thread
julio-rocketchat marked this conversation as resolved.
Resolved
transformer = sharp(buffer, { pages }).resize({ width, height, fit: 'inside' });
Comment thread
julio-rocketchat marked this conversation as resolved.
Resolved
} else {
const image = await store._store.getReadStream(file._id, file);
transformer = sharp().resize({ width, height, fit: 'inside' });
image.pipe(transformer);
}

if (file.type === 'image/svg+xml') {
transformer = transformer.png();
}
const result = transformer.toBuffer({ resolveWithObject: true }).then(({ data, info: { width, height, format } }) => ({
const result = transformer.toBuffer({ resolveWithObject: true }).then(({ data, info: { width, height, pageHeight, format } }) => ({
data,
width,
height,
height: pageHeight ?? height,
thumbFileType: (mime.lookup(format) as string) || '',
thumbFileName: file?.name as string,
originalFileId: file?._id as string,
}));
image.pipe(transformer);

return result;
},
Expand Down
Loading