fix(upload): decode mojibake of Thai filenames
อัปโหลดไฟล์ชื่อไทยแล้ว title ที่แสดงในรายการเพี้ยนเป็น latin1
mojibake ("คำร้องทดสอบอัปโหลด.pdf" → "คำรเน‰à¸à¸‡…") — คำขอผู้ใช้ 2026-10-05
(1) สาเหตุ: browser ใส่ชื่อไฟล์ (UTF-8) ลง header Content-Disposition
ของ multipart body ตรง ๆ แต่ undici ของ Node (ตัว parse
request.formData() ที่ Next.js ใช้) ถอดรหัส header เป็น latin1
ทำให้ file.name ใน Server Action เพี้ยนก่อนถึงโค้ด — เนื้อไฟล์
ไม่เกี่ยว (ผ่าน arrayBuffer() ตรง ๆ) พิสูจน์ด้วย Playwright
อัปโหลดจริงก่อนแก้ได้สตริง mojibake ตรงกับที่ผู้ใช้รายงาน
(2) แก้: utils/upload-name.ts ใหม่ — decodeUploadFilename เข้ารหัส
ชื่อกลับเป็น byte แล้วถอดด้วย UTF-8 ให้คืนชื่อเดิม พร้อม guard
2 ชั้นกันแตะชื่อที่ถูกต้องอยู่แล้ว: ไม่มีตัวอักษรช่วง
U+0080–U+00FF เลย = ASCII ล้วน/ชื่อถูกอยู่แล้ว (ไทย ≥ U+0E00)
ไม่แต้อยู่เดิม / ถอดแล้วได้ U+FFFD (ไบต์ไม่ประกอบ UTF-8 สมบูรณ์
= ชื่อเป็น latin1 จริง) เก็บของเดิม — uploadFile เรียกครั้งเดียว
ที่จุดเข้า ก่อนใช้ทั้ง allowedExt และ sanitizeTitle
(3) tests: tests/utils/upload-name.test.ts ใหม่ 5 เคส (ชื่อไทย
mojibake → คืนเดิม / ไทยผสมตัวเลขวงเล็บ / ASCII ไม่เปลี่ยน /
ชื่อถูกอยู่แล้วไม่ถูกแตะ / latin1 จริงเก็บของเดิม)
(4) code review 2026-10-05: ยอมรับความเสี่ยง false positive กรณีชื่อ
latin1 จริงที่ byte คู่กันบังเอิญประกอบเป็น UTF-8 ครบ ("¢.pdf")
— แคบมากเพราะ browser ส่งชื่อเป็น UTF-8 เสมอ และ heuristic นี้
คือ workaround มาตรฐานของปัญหา undici (บีบ guard เพิ่มจะทำให้
ชื่อ latin1-accent ที่โดน mojibake จริง ๆ กลับไม่ถูกซ่อม)
(5) docs sync: CLAUDE.md (tree entry) + rules/data-layer.md (กติกา
uploadFile)
ผ่าน tsc + vitest + ยืนยันใน browser ว่าการ์ดแสดงชื่อไทยตรงเป๊ะ
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
e4ecc026b0
commit
c08aaab1ed
5 changed files with 60 additions and 3 deletions
|
|
@ -13,6 +13,7 @@ import { db } from "@/db";
|
|||
import { files } from "@/db/schema";
|
||||
import { normDocumentGroup } from "@/utils/external-file";
|
||||
import { SAMPLE_FILE_EXTENSIONS } from "@/utils/file-kind";
|
||||
import { decodeUploadFilename } from "@/utils/upload-name";
|
||||
|
||||
const MAX_UPLOAD_BYTES = 100 * 1024 * 1024; // 100MB (ตาม bodySizeLimit ใน next.config.mjs)
|
||||
const UPLOAD_DIR = path.join(process.cwd(), "public", "uploads");
|
||||
|
|
@ -67,7 +68,10 @@ export async function uploadFile(formData: FormData): Promise<UploadFileResult>
|
|||
if (file.size === 0) {
|
||||
return { ok: false, error: "ไฟล์ว่าง (0 ไบต์) — เลือกไฟล์ที่สมบูรณ์" };
|
||||
}
|
||||
const ext = allowedExt(file.name);
|
||||
// ชื่อไฟล์ที่ browser ส่งมาโดน undici ถอดรหัสเป็น latin1 (ชื่อไทยเพี้ยน) —
|
||||
// ถอดกลับก่อนใช้ทั้งตรวจ extension และตั้ง title
|
||||
const fileName = decodeUploadFilename(file.name);
|
||||
const ext = allowedExt(fileName);
|
||||
if (!ext) {
|
||||
return {
|
||||
ok: false,
|
||||
|
|
@ -93,7 +97,7 @@ export async function uploadFile(formData: FormData): Promise<UploadFileResult>
|
|||
const diskPath = path.join(UPLOAD_DIR, diskName);
|
||||
await writeFile(diskPath, bytes);
|
||||
|
||||
const title = sanitizeTitle(file.name);
|
||||
const title = sanitizeTitle(fileName);
|
||||
const pdfUrl = `/uploads/${diskName}`;
|
||||
const typeId = normDocumentGroup(formData.get("typeId"));
|
||||
try {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue