import { FastifyPluginAsync } from 'fastify'; import { eq } from 'drizzle-orm'; import { db } from '../config/database.js'; import { contentSections } from '../db/schema.js'; import { AssetService } from '../services/asset.service.js'; import { isManagedAsset, forgetManagedAsset } from '../services/managed-assets.service.js'; import { saveUploadedPdf } from '../services/upload.service.js'; import { env } from '../config/env.js'; const assets = new AssetService(); /** * Feste PDF-Plaetze. Die URL landet jeweils in einer Content-Section, damit * der Generator sie beim Publish in die Astro-Komponente schreiben kann. */ const PDF_SLOTS: Record = { drinks: { section: 'drinks', field: 'pdfUrl' }, }; /** contentJson kommt je nach Treiber als Objekt oder als String zurueck. */ function asObject(value: any): Record { if (!value) return {}; if (typeof value === 'string') { try { const parsed = JSON.parse(value); return parsed && typeof parsed === 'object' ? parsed : {}; } catch { return {}; } } return typeof value === 'object' ? value : {}; } const pdfRoute: FastifyPluginAsync = async (fastify) => { // PDF fuer einen festen Platz hochladen und verlinken fastify.post('/pdf/:slot', { preHandler: [fastify.authenticate], }, async (request, reply) => { const { slot } = request.params as { slot: string }; const target = PDF_SLOTS[slot]; if (!target) { return reply.code(404).send({ error: `Unknown PDF slot "${slot}"` }); } try { const file = await (request as any).file({ limits: { fileSize: env.MAX_PDF_SIZE } }); if (!file) { return reply.code(400).send({ error: 'No file uploaded' }); } const mime = file.mimetype as string | undefined; const originalName = (file.filename as string | undefined) || ''; if (mime !== 'application/pdf' && !originalName.toLowerCase().endsWith('.pdf')) { return reply.code(400).send({ error: 'Only PDF uploads are allowed' }); } const chunks: Buffer[] = []; for await (const chunk of file.file) { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); } const buffer = Buffer.concat(chunks); if ((file.file as any)?.truncated) { const limit = Math.round(env.MAX_PDF_SIZE / 1024 / 1024); return reply.code(413).send({ error: `PDF too large. Maximum is ${limit} MB` }); } // Inhalt gegenpruefen, damit nicht irgendetwas mit .pdf-Endung landet if (buffer.subarray(0, 5).toString('latin1') !== '%PDF-') { return reply.code(400).send({ error: 'File is not a valid PDF' }); } const pdfUrl = await saveUploadedPdf(file, buffer); // URL in der Content-Section hinterlegen const [existing] = await db .select() .from(contentSections) .where(eq(contentSections.sectionName, target.section)) .limit(1); const previousContent = asObject((existing as any)?.contentJson); const previousUrl = previousContent[target.field]; const content = { ...previousContent, [target.field]: pdfUrl }; if (existing) { await db .update(contentSections) .set({ contentJson: content, updatedAt: new Date() }) .where(eq(contentSections.sectionName, target.section)); } else { await db .insert(contentSections) .values({ sectionName: target.section, contentJson: content }); } // Vorgaenger wegraeumen - greift nur bei frueher hochgeladenen PDFs, // die mitgelieferte Getraenkekarte aus dem Repo bleibt liegen if (previousUrl && previousUrl !== pdfUrl && (await isManagedAsset(previousUrl))) { try { if (assets.deletePdf(previousUrl)) { fastify.log.info(`Removed replaced PDF ${previousUrl}`); } await forgetManagedAsset(previousUrl); } catch (err) { fastify.log.warn({ err }, 'Could not remove replaced PDF'); } } return reply.code(201).send({ pdfUrl, section: target.section }); } catch (err: any) { if (err?.code === 'FST_REQ_FILE_TOO_LARGE') { const limit = Math.round(env.MAX_PDF_SIZE / 1024 / 1024); return reply.code(413).send({ error: `PDF too large. Maximum is ${limit} MB` }); } fastify.log.error({ err }, 'PDF upload failed'); return reply.code(500).send({ error: 'Failed to upload PDF' }); } }); }; export default pdfRoute;