Add browser security headers, implement upload temp directory management, and enhance SSE handling.
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
* @file Validiert und speichert Bildanhänge und bereitet zwischengespeicherte Bildanalysen für den Chat vor.
|
||||
*/
|
||||
import sharp from "sharp";
|
||||
import {readFile, unlink} from "node:fs/promises";
|
||||
import {createHash, randomUUID} from "node:crypto";
|
||||
import {attachmentMetadata} from "../db/attachments.js";
|
||||
import {LocalFileStorage} from "./localFileStorage.js";
|
||||
@@ -164,26 +165,29 @@ export function createAttachmentService(db, config, modelService, visionDebugSer
|
||||
},
|
||||
async upload(file, user, {forImport = false} = {}) {
|
||||
await cleanup();
|
||||
if (!file?.buffer?.length) throw new AttachmentError("Choose a non-empty PNG, JPEG or WebP image");
|
||||
if (file.size > config.uploadMaxSizeMb * 1024 * 1024) throw new AttachmentError("Image is too large", 413);
|
||||
let buffer;
|
||||
try { buffer = file?.buffer || (file?.path ? await readFile(file.path) : null); }
|
||||
finally { if (file?.path) await unlink(file.path).catch(() => {}); }
|
||||
if (!buffer?.length) throw new AttachmentError("Choose a non-empty PNG, JPEG or WebP image");
|
||||
if (buffer.length > config.uploadMaxSizeMb * 1024 * 1024) throw new AttachmentError("Image is too large", 413);
|
||||
let info;
|
||||
try {
|
||||
const image = sharp(file.buffer, {limitInputPixels: config.uploadMaxPixels, failOn: "warning"});
|
||||
const image = sharp(buffer, {limitInputPixels: config.uploadMaxPixels, failOn: "warning"});
|
||||
info = await image.metadata();
|
||||
if (!["png", "jpeg", "webp"].includes(info.format) || (info.pages || 1) > 1) throw new Error("Unsupported");
|
||||
await image.stats(); // Decode pixels, not just headers: reject truncated/malformed images.
|
||||
} catch { throw new AttachmentError("Invalid image. Use a non-animated PNG, JPEG or WebP within the pixel limit."); }
|
||||
const {storageKey} = await storage.save({buffer: file.buffer, extension: info.format}).catch(() => { throw new AttachmentError("Image could not be saved. Please retry.", 503); });
|
||||
const {storageKey} = await storage.save({buffer, extension: info.format}).catch(() => { throw new AttachmentError("Image could not be saved. Please retry.", 503); });
|
||||
const id = randomUUID();
|
||||
try {
|
||||
// Bound abandoned uploads per account as well as per submitted message.
|
||||
db.transaction(() => {
|
||||
if (!forImport && db.prepare("SELECT COUNT(*) AS n FROM attachments WHERE username=? AND status='temporary'").get(user).n >= config.uploadMaxFiles * 4) throw new AttachmentError("Too many pending images. Remove unused uploads first.");
|
||||
db.prepare("INSERT INTO attachments(id,username,original_name,storage_key,mime_type,size_bytes,width,height) VALUES(?,?,?,?,?,?,?,?)").run(id, user,
|
||||
[...String(file.originalname || "image").replace(/[\\/]/g, "_")].map(character => character.charCodeAt(0) < 32 ? "_" : character).join("").slice(0, 255), storageKey, `image/${info.format}`, file.size, info.width, info.height);
|
||||
[...String(file.originalname || "image").replace(/[\\/]/g, "_")].map(character => character.charCodeAt(0) < 32 ? "_" : character).join("").slice(0, 255), storageKey, `image/${info.format}`, buffer.length, info.width, info.height);
|
||||
})();
|
||||
} catch (error) { await storage.delete(storageKey); throw error; }
|
||||
log("attachmentUploadSuccess", {id, size: file.size, mime: `image/${info.format}`});
|
||||
log("attachmentUploadSuccess", {id, size: buffer.length, mime: `image/${info.format}`});
|
||||
return attachmentMetadata(get(id));
|
||||
},
|
||||
createMessage(ids, user, chatId, insert, reuseChatId = null) {
|
||||
|
||||
Reference in New Issue
Block a user