import { prisma } from "@/lib/prisma"; import { s3 } from "@/lib/s3"; import { GetObjectCommand } from "@aws-sdk/client-s3"; import archiver from "archiver"; import { NextRequest } from "next/server"; type Mode = "display" | "download" | "bulk"; function contentDisposition(filename: string, mode: Mode) { const type = mode === "display" ? "inline" : "attachment"; const safe = filename.replace(/["\\]/g, "_"); const encoded = encodeURIComponent(filename); return `${type}; filename="${safe}"; filename*=UTF-8''${encoded}`; } function sanitizeZipEntryName(name: string) { return name.replace(/[^\w.\- ()\[\]]+/g, "_").slice(0, 180); } export async function GET(req: NextRequest) { try { const bucket = process.env.BUCKET_NAME; if (!bucket) return new Response("BUCKET_NAME is not set", { status: 500 }); const mode = (req.nextUrl.searchParams.get("mode") ?? "display") as Mode; // ---- Single file: display/download ---- if (mode === "display" || mode === "download") { const fileId = req.nextUrl.searchParams.get("fileId"); if (!fileId) return new Response("Missing fileId", { status: 400 }); const file = await prisma.commissionRequestFile.findUnique({ where: { id: fileId }, select: { fileKey: true, originalFile: true, fileType: true, fileSize: true, }, }); if (!file) return new Response("Not found", { status: 404 }); const s3Res = await s3.send( new GetObjectCommand({ Bucket: bucket, Key: file.fileKey, }) ); if (!s3Res.Body) return new Response("No body", { status: 500 }); const contentType = file.fileType || s3Res.ContentType || "application/octet-stream"; return new Response(s3Res.Body as ReadableStream, { headers: { "Content-Type": contentType, // You can tune caching; admin-only content usually should be private. "Cache-Control": mode === "display" ? "private, max-age=300" : "private, max-age=0, no-store", "Content-Disposition": contentDisposition(file.originalFile || "file", mode), }, }); } // ---- Bulk zip for a request ---- if (mode === "bulk") { const requestId = req.nextUrl.searchParams.get("requestId"); if (!requestId) return new Response("Missing requestId", { status: 400 }); const request = await prisma.commissionRequest.findUnique({ where: { id: requestId }, select: { id: true, createdAt: true, files: { orderBy: { createdAt: "asc" }, select: { fileKey: true, originalFile: true, fileType: true, }, }, }, }); if (!request) return new Response("Not found", { status: 404 }); if (!request.files.length) return new Response("No files", { status: 404 }); const zipName = `commission-${request.id}.zip`; const archive = archiver("zip", { zlib: { level: 9 } }); const stream = new ReadableStream({ start(controller) { archive.on("data", (chunk) => controller.enqueue(chunk)); archive.on("end", () => controller.close()); archive.on("error", (err) => controller.error(err)); }, cancel() { archive.destroy(); }, }); (async () => { try { for (const f of request.files) { const obj = await s3.send( new GetObjectCommand({ Bucket: bucket, Key: f.fileKey, }) ); if (!obj.Body) continue; const entryName = sanitizeZipEntryName( f.originalFile || f.fileKey.split("/").pop() || "file" ); // obj.Body is a Node stream in Node runtime; works with archiver archive.append(obj.Body as any, { name: entryName }); } await archive.finalize(); } catch (err) { archive.destroy(err as Error); } })(); return new Response(stream, { headers: { "Content-Type": "application/zip", "Content-Disposition": `attachment; filename="${zipName}"`, "Cache-Control": "private, max-age=0, no-store", }, }); } return new Response("Invalid mode", { status: 400 }); } catch (err) { console.error("[GET /api/requests/image] failed", err); return new Response("Internal error", { status: 500 }); } }