145 lines
4.5 KiB
TypeScript
145 lines
4.5 KiB
TypeScript
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 });
|
|
}
|
|
}
|