import { Hono } from 'hono'; import { mkdir, writeFile } from 'node:fs/promises'; import path from 'node:path'; import sharp from 'sharp'; // Bild-Upload → static/images/. Raster-Bilder werden zu WebP konvertiert // (kleiner, web-optimiert), auf max. 2000px begrenzt, EXIF-Rotation korrigiert. // SVG/GIF bleiben unangetastet (Vektor/Animation erhalten). // // Sicherheit: hartes Größenlimit (DoS / Decompression-Bombs), Raster wird über // sharp-Metadaten als echtes Bild verifiziert, SVG nur wenn es wie SVG aussieht. // Hochgeladene Dateien werden zudem mit strikter CSP (sandbox) ausgeliefert // (siehe index.js, /images/*) → ein bösartiges SVG kann kein JS im Origin starten. const SITE_DIR = process.env.SITE_DIR || '/site'; const MAX_UPLOAD = 8 * 1024 * 1024; // 8 MB Rohdatei const ALLOWED_RASTER = new Set(['jpeg', 'png', 'webp', 'avif', 'tiff']); const upload = new Hono(); upload.post('/', async (c) => { const body = await c.req.parseBody(); const file = body['file']; if (!file || typeof file === 'string') return c.json({ error: 'Keine Datei' }, 400); if (typeof file.size === 'number' && file.size > MAX_UPLOAD) { return c.json({ error: 'Datei zu groß (max. 8 MB)' }, 413); } const buffer = Buffer.from(await file.arrayBuffer()); if (buffer.length > MAX_UPLOAD) return c.json({ error: 'Datei zu groß (max. 8 MB)' }, 413); if (!buffer.length) return c.json({ error: 'Leere Datei' }, 400); const dir = path.join(SITE_DIR, 'static', 'images'); await mkdir(dir, { recursive: true }); const ext = path.extname(file.name || '').toLowerCase(); const base = `${safeBase(file.name)}-${uid()}`; let outName, outBuf; if (ext === '.svg') { // Muss wie SVG aussehen (Magie/Marker), sonst ablehnen. const head = buffer.subarray(0, 512).toString('utf8').trimStart().toLowerCase(); if (!head.startsWith('