Add request single page
This commit is contained in:
144
src/app/api/requests/image/route.ts
Normal file
144
src/app/api/requests/image/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user