/** * @file Verarbeitet das Hochladen, Abrufen und Entfernen von Bildanhängen mit Zugriffskontrolle. */ import express from "express"; import {mkdirSync} from "node:fs"; import {unlink} from "node:fs/promises"; import {asyncHandler} from "../middleware/asyncHandler.js"; import multer from "multer"; import {requireLogin} from "../middleware/auth.js"; import {AttachmentError} from "../services/attachmentService.js"; export function createUploadsRouter(service, config) { const router = express.Router(); mkdirSync(config.uploadTmpDir, {recursive: true}); const activeUploads = new Map(); const upload = multer({storage: multer.diskStorage({destination: config.uploadTmpDir}), limits: { fileSize: config.uploadMaxSizeMb * 1024 * 1024, files: 1, fields: 0, parts: 1 }}).single("image"); const fail = (res, error) => res.status(error instanceof AttachmentError ? error.status : 500).json({error: error instanceof AttachmentError ? error.message : "Image operation failed. Please retry."}); router.get("/uploads/config", requireLogin, (_req, res) => res.json({maxSizeMb: config.uploadMaxSizeMb, maxFiles: config.uploadMaxFiles})); router.post("/uploads", requireLogin, (req, res) => { const user = req.session.user; const active = activeUploads.get(user) || 0; if (active >= config.uploadMaxFiles) return res.status(429).json({error: "Too many simultaneous uploads. Please wait."}); activeUploads.set(user, active + 1); res.once("close", () => { const remaining = (activeUploads.get(user) || 1) - 1; if (remaining) activeUploads.set(user, remaining); else activeUploads.delete(user); }); upload(req, res, async error => { if (error) { if (req.file?.path) await unlink(req.file.path).catch(() => {}); return res.status(error.code === "LIMIT_FILE_SIZE" ? 413 : 400).json({error: error.code === "LIMIT_FILE_SIZE" ? `Image exceeds ${config.uploadMaxSizeMb} MB` : "Upload one PNG, JPEG or WebP image per request"}); } try { res.status(201).json(await service.upload(req.file, req.session.user)); } catch (failure) { fail(res, failure); } }); }); router.get("/attachments/:id", requireLogin, asyncHandler(async (req, res) => { try { const row = service.owned(req.params.id, req.session.user); const buffer = await service.storage.read(row.storage_key); res.set({"Content-Type": row.mime_type, "Content-Length": buffer.length, "Cache-Control": "private, no-store", "X-Content-Type-Options": "nosniff", "Content-Disposition": "inline"}).send(buffer); } catch (error) { fail(res, error); } })); router.delete("/attachments/:id", requireLogin, asyncHandler(async (req, res) => { try { await service.remove(req.params.id, req.session.user); res.json({success: true}); } catch (error) { fail(res, error); } })); return router; }