SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
3.8 KB · 66 lines typescript
Raw Blame History
1import { withUser, json, ApiError } from "@/lib/api";2import { getDb, messageAttachments } from "@/db";3import { ids } from "@/lib/ids";4import { IMAGE_MIME, isTextLike } from "@/lib/ai/core/content";5import { LIMITS } from "@/lib/rate-limit";6import { and, eq } from "drizzle-orm";78export const dynamic = "force-dynamic";910const MAX_BYTES = 10 * 1024 * 1024;11const MAX_TEXT_BYTES = 2 * 1024 * 1024;1213function kindOf(mime: string, name: string): string | null {14  if (IMAGE_MIME.has(mime)) return "image";15  if (mime === "application/pdf" || /\.pdf$/i.test(name)) return "pdf";16  if (mime === "text/csv" || /\.csv$/i.test(name)) return "csv";17  if (mime === "application/json" || /\.json$/i.test(name)) return "json";18  if (isTextLike(mime, name)) return /\.(md|txt)$/i.test(name) ? "text" : "code";19  return null;20}2122function pngSize(buf: Buffer): { width: number; height: number } | null {23  if (buf.length > 24 && buf.toString("ascii", 1, 4) === "PNG") return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };24  return null;25}2627/** POST /api/attachments — multipart upload; returns attachment metadata (payload stays server-side). */28export const POST = withUser(29  async ({ req, user }) => {30    const len = Number(req.headers.get("content-length") ?? 0);31    if (len > MAX_BYTES + 4096) throw new ApiError(413, "File too large (max 10 MB)", "PAYLOAD_TOO_LARGE");32    const form = await req.formData().catch(() => null);33    const file = form?.get("file");34    if (!(file instanceof File)) throw new ApiError(400, "Missing file", "BAD_REQUEST");35    const name = (file.name || "file").slice(0, 200);36    const mime = (file.type || "application/octet-stream").toLowerCase();37    const kind = kindOf(mime, name);38    if (!kind) throw new ApiError(415, `Unsupported file type (${mime || "unknown"}). Use images, PDF, text, code, CSV or JSON.`, "UNSUPPORTED_TYPE");39    if (file.size > MAX_BYTES) throw new ApiError(413, "File too large (max 10 MB)", "PAYLOAD_TOO_LARGE");40    if (kind !== "image" && kind !== "pdf" && file.size > MAX_TEXT_BYTES) throw new ApiError(413, "Text files are limited to 2 MB", "PAYLOAD_TOO_LARGE");41    const buf = Buffer.from(await file.arrayBuffer());42    const dims = kind === "image" ? pngSize(buf) : null;43    const effectiveMime = kind === "image" ? mime : kind === "pdf" ? "application/pdf" : mime.startsWith("text/") || mime === "application/json" ? mime : "text/plain";44    const [row] = await getDb()45      .insert(messageAttachments)46      .values({ id: ids.attachment(), userId: user.id, kind, name, mimeType: effectiveMime, sizeBytes: buf.length, dataBase64: buf.toString("base64"), width: dims?.width ?? null, height: dims?.height ?? null })47      .returning({ id: messageAttachments.id, kind: messageAttachments.kind, name: messageAttachments.name, mimeType: messageAttachments.mimeType, sizeBytes: messageAttachments.sizeBytes, width: messageAttachments.width, height: messageAttachments.height });48    return json({ attachment: row }, { status: 201 });49  },50  { limit: { ...LIMITS.upload, key: "upload" } },51);5253/** GET /api/attachments?id= — streams the payload back to its owner (used for image previews). */54export const GET = withUser(async ({ req, user }) => {55  const id = new URL(req.url).searchParams.get("id");56  if (!id) throw new ApiError(400, "Missing id");57  const [row] = await getDb()58    .select()59    .from(messageAttachments)60    .where(and(eq(messageAttachments.id, id), eq(messageAttachments.userId, user.id)))61    .limit(1);62  if (!row) throw new ApiError(404, "Not found", "NOT_FOUND");63  const buf = Buffer.from(row.dataBase64, "base64");64  return new Response(buf, { headers: { "Content-Type": row.mimeType, "Content-Length": String(buf.length), "Cache-Control": "private, max-age=3600", "Content-Disposition": `inline; filename="${encodeURIComponent(row.name)}"` } });65});66