import { readFile } from "node:fs/promises"; import { fileKindFromUrl, imageContentTypeFromUrl, SAMPLE_FILE_EXTENSIONS, } from "@/utils/file-kind"; import { PdfPathError, resolvePublicPdfPath } from "@/lib/server-pdf"; // N4 — เสิร์ฟไฟล์ที่อัปโหลด (`public/uploads/.`) ผ่าน route handler // ไม่ใช่ static file ของ Next: production server (next start / standalone) จะไล่ // รายชื่อไฟล์ใน public/ เก็บไว้ใน Set **ครั้งเดียวตอน startup** เท่านั้น // (next/dist/server/lib/router-utils/filesystem.js — setupFsCheck) ไฟล์ที่ action // เขียนลง public/uploads/ หลัง server รันแล้วจึง GET ตรง ๆ ไม่ได้ (404) // ฝั่ง next dev ไม่ผ่าน branch นั้น เลยทำงานได้ — อาการคลาสสิก "local ได้ production พัง" // อ่านอย่างเดียว (invariant #1) + path guard ของ lib/server-pdf เหมือน /api/pdf (§5) // ชื่อไฟล์ต้องเป็น uuid + extension ใน whitelist เท่านั้น (ตรงรูปที่ uploadFile เขียน) // — ชื่ออื่น 404 กลับไป ไม่รู้จักก็ไม่เปิด (กัน path traversal โดยแบบชื่อเอง: อักขระ // นอก hex/ขีด/จุด ผ่าน regex นี้ไม่ได้) const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; export const dynamic = "force-dynamic"; export async function GET( _req: Request, { params }: { params: { name: string } } ) { const dot = params.name.lastIndexOf("."); if (dot <= 0) return new Response(null, { status: 404 }); const [id, ext] = [ params.name.slice(0, dot), params.name.slice(dot).toLowerCase(), ]; if (!UUID_RE.test(id)) return new Response(null, { status: 404 }); // ext ต้องอยู่ใน whitelist เดียวกับ uploadFile — ชื่ออื่น 404 ไม่เสิร์ฟ if (!SAMPLE_FILE_EXTENSIONS.includes(ext)) { return new Response(null, { status: 404 }); } // helper เหล่านี้คาดชื่อไฟล์เต็ม (จุดต้องอยู่หลังอักขระตัวแรก) — ส่งชื่อเต็ม ไม่ใช่ bare ext const contentType = fileKindFromUrl(params.name) === "image" ? imageContentTypeFromUrl(params.name) : "application/pdf"; let bytes: Buffer; try { // guard: resolve แล้วต้องอยู่ใน public/ เท่านั้น (รูปแบบชื่อผ่าน regex อยู่แล้ว // — ชั้นนี้กันไว้เผื่อมีคนเรียก helper นี้ซ้ำในอนาคตด้วย input อื่น) const diskPath = resolvePublicPdfPath(`/uploads/${id}${ext}`); bytes = await readFile(diskPath); } catch (err) { if (err instanceof PdfPathError) return new Response(null, { status: 404 }); if ((err as NodeJS.ErrnoException).code === "ENOENT") { return new Response(null, { status: 404 }); } throw err; } return new Response(new Uint8Array(bytes), { headers: { "Content-Type": contentType, // ชื่อไฟล์เป็น uuid ต่อการอัปโหลดหนึ่งครั้ง ไม่มีการเขียนทับ — ค่าเดียวกันตลอดชีวิตไฟล์ "Cache-Control": "public, max-age=31536000, immutable", }, }); }