Add request single page

This commit is contained in:
2026-01-01 11:52:24 +01:00
parent 42f23dddcf
commit 2fcf19c0df
13 changed files with 1007 additions and 83 deletions

View File

@ -0,0 +1,144 @@
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 });
}
}