import { createRouter } from '../../create-router'; import { mkdir } from 'node:fs/promises'; import { join } from 'node:path'; import { getTmpAttachmentsDir, getAttachmentsDir } from '@@/data-path'; import { writeAtomic } from '@@/write-atomic'; const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5 MB /** PDFs never enter a prompt whole — the agent reads them from disk — so the ceiling is storage, not context. */ const PDF_MAX_FILE_SIZE = 25 * 1024 * 1024; // 25 MB export const uploadRouter = createRouter(); uploadRouter.post('/', async (ctx) => { const user = ctx.get('user'); const body = ctx.get('body') as Record; const file = body.file as File | null; const sessionId = (body.sessionId as string) || null; if (!file || !(file instanceof File)) { return ctx.json({ error: 'file is required' }, 400); } // Images and PDFs. The two are handled very differently downstream and it is worth knowing which is // which here: an image travels to the model as base64 in the message, while a PDF is left on disk and // the agent is told where it is, because reading a document is something its own tools already do well // (page ranges, extraction) and a 10 MB base64 blob in a prompt is not. const isImage = file.type.startsWith('image/'); const isPdf = file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf'); if (!isImage && !isPdf) { return ctx.json({ error: 'Only images and PDFs are allowed' }, 400); } // A PDF is allowed to be bigger: 5 MB is a generous photo and a small report. The agent reads it from // disk in page ranges, so size costs storage rather than context. const limit = isPdf ? PDF_MAX_FILE_SIZE : MAX_FILE_SIZE; if (file.size > limit) { return ctx.json({ error: `File exceeds ${Math.round(limit / 1024 / 1024)} MB limit` }, 400); } try { const buffer = Buffer.from(await file.arrayBuffer()); const ext = file.name.split('.').pop() || 'png'; const filename = `${crypto.randomUUID()}.${ext}`; let saveDir: string; if (sessionId) { saveDir = getAttachmentsDir(user.email, sessionId); } else { saveDir = getTmpAttachmentsDir(user.email); } await writeAtomic(join(saveDir, filename), buffer); // Only an image gets a data URL. Building one for a PDF would mean carrying megabytes through the // browser for a preview nothing renders. const dataUrl = isImage ? `data:${file.type};base64,${buffer.toString('base64')}` : ''; return ctx.json({ filename: file.name, dataUrl, attachmentId: filename, kind: isPdf ? 'pdf' : 'image', }); } catch (err) { const message = err instanceof Error ? err.message : 'Upload failed'; return ctx.json({ error: message }, 500); } });