spb/note-spboucher-ai
Public
1import { handler, HttpError } from "@/lib/api";2import { db, schema } from "@/db";3import { and, desc, eq, isNull } from "drizzle-orm";4import { storeFile } from "@/lib/files/storage";5import { extractText } from "@/lib/files/extract";6import { enqueue, QUEUES } from "@/lib/jobs";7import { audit } from "@/lib/audit";8const MAX = 200 * 1024 * 1024;9const BLOCKED = new Set(["application/x-msdownload", "application/x-sh", "text/html", "application/javascript"]);10export const POST = handler(async ({ req, ws, user }) => {11 const form = await req.formData(); const file = form.get("file"); const pageId = (form.get("pageId") as string) || null;12 if (!(file instanceof File)) throw new HttpError(400, "file missing");13 if (file.size > MAX) throw new HttpError(413, "File too large (max 200 MB)");14 const mime = file.type || "application/octet-stream"; if (BLOCKED.has(mime)) throw new HttpError(415, "File type not allowed");15 const buf = Buffer.from(await file.arrayBuffer());16 const { sha256, storagePath } = await storeFile(buf, file.name);17 const ex = await extractText(buf, mime, file.name);18 const [a] = await db.insert(schema.attachments).values({ workspaceId: ws.id, pageId, filename: file.name.slice(0, 255), mime, size: file.size, sha256, storagePath, extractedText: ex?.text ?? null, pageCount: ex?.pages ?? null }).returning();19 if (ex?.text) await enqueue(QUEUES.index, { sourceType: "attachment", sourceId: a.id, workspaceId: ws.id });20 await audit(user.id, "attachment.upload", { type: "attachment", id: a.id }, { name: a.filename, size: a.size });21 return Response.json({ id: a.id, url: `/api/files/${a.id}`, name: a.filename, size: a.size, mime: a.mime, hasText: Boolean(ex?.text), pages: ex?.pages ?? null }, { status: 201 });22});23export const GET = handler(async ({ req, ws }) => {24 const pageId = new URL(req.url).searchParams.get("pageId");25 const rows = await db.select({ id: schema.attachments.id, filename: schema.attachments.filename, mime: schema.attachments.mime, size: schema.attachments.size, pageId: schema.attachments.pageId, pageCount: schema.attachments.pageCount, createdAt: schema.attachments.createdAt, hasText: schema.attachments.extractedText }).from(schema.attachments).where(and(eq(schema.attachments.workspaceId, ws.id), isNull(schema.attachments.deletedAt), pageId ? eq(schema.attachments.pageId, pageId) : undefined)).orderBy(desc(schema.attachments.createdAt)).limit(200);26 return Response.json(rows.map((r) => ({ ...r, hasText: Boolean(r.hasText), url: `/api/files/${r.id}` })));27});28export const DELETE = handler(async ({ req, ws, user }) => {29 const id = new URL(req.url).searchParams.get("id")!;30 await db.update(schema.attachments).set({ deletedAt: new Date() }).where(and(eq(schema.attachments.id, id), eq(schema.attachments.workspaceId, ws.id)));31 await db.delete(schema.chunks).where(and(eq(schema.chunks.sourceType, "attachment"), eq(schema.chunks.sourceId, id)));32 await audit(user.id, "attachment.delete", { type: "attachment", id }); return Response.json({ ok: true });33});34