import { count, eq } from 'drizzle-orm' import type { Config } from '../config.ts' import type { Db } from '../db/client.ts' import { attachments } from '../db/schema.ts' import type { FileStorage } from '../storage.ts' import { detectFileType } from './file-type.ts' import { fail } from './http.ts' export const MAX_ATTACHMENTS_PER_INCIDENT = 20 /** Validates and stores one uploaded file against an incident. */ export async function saveAttachment( { db, storage, config }: { db: Db; storage: FileStorage; config: Config }, incidentId: string, file: unknown, uploadedById: string | null, ) { if (!(file instanceof File)) throw fail(400, 'Attach a file in the "file" field') if (file.size > config.maxUploadBytes) throw fail(413, `Files can be at most ${Math.round(config.maxUploadBytes / 1024 / 1024)} MB`) const [{ existing }] = await db.select({ existing: count() }).from(attachments).where(eq(attachments.incidentId, incidentId)) if (existing >= MAX_ATTACHMENTS_PER_INCIDENT) throw fail(409, `An incident can have at most ${MAX_ATTACHMENTS_PER_INCIDENT} attachments`) const data = new Uint8Array(await file.arrayBuffer()) const type = detectFileType(data) if (!type) throw fail(400, 'Only photos (JPEG, PNG, WebP, GIF, HEIC) and PDFs can be attached') const storageKey = await storage.save(data, type.extension) const fileName = (file.name || `attachment.${type.extension}`).replace(/[\r\n"\\/]/g, '_').slice(0, 200) try { const [row] = await db .insert(attachments) .values({ incidentId, fileName, contentType: type.contentType, sizeBytes: data.byteLength, storageKey, uploadedById }) .returning() return row } catch (error) { await storage.remove(storageKey) throw error } }