70 lines
3.8 KiB
TypeScript
70 lines
3.8 KiB
TypeScript
|
|
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/<uuid>.<ext>`) ผ่าน 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",
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|