อัปโหลดไฟล์/รูปภาพ
ภาพรวม
ระบบมีบริการอัปโหลดไฟล์แบบชั่วคราว (temp) ที่ใช้ร่วมกันระหว่างสองฟีเจอร์หลัก คือ field ชนิดอัปโหลดไฟล์ในฟอร์ม และการแนบรูปภาพในโพสต์หรือคอมเมนต์บนบอร์ดประกาศ
หลักการทำงานคือ ไฟล์จะถูกอัปโหลดขึ้น API ทันทีที่ผู้ใช้เลือกไฟล์ แล้วเก็บเฉพาะ path หรือ URL ที่ได้กลับมาไว้ในค่าของ field หรือใน payload ของโพสต์ การผูกไฟล์ชั่วคราวเข้ากับ record จริงเกิดขึ้นที่ฝั่ง API ในขั้นตอนบันทึกข้อมูล
Business Flow
- ผู้ใช้เลือกไฟล์ ซึ่งอาจมาจากคอมโพเนนต์อัปโหลดของ Ant Design ในฝั่งฟอร์ม หรือจาก file input มาตรฐานในฝั่งบอร์ดประกาศ
- คอมโพเนนต์เรียก handler การอัปโหลดของตัวเอง โดยไม่ใช้กลไกอัปโหลดเริ่มต้นของ Ant Design เพื่อควบคุมการเรียก API และการจัดการข้อผิดพลาดได้เอง
- ระบบประกอบข้อมูลแล้วเรียก upload service ซึ่งเลือกปลายทางตามรูปแบบข้อมูลที่ส่ง
- ส่งเป็น FormData จะไปที่
POST /upload-file/tempแบบ multipart - ส่งเป็นสตริง base64 จะไปที่
POST /upload-file/temp-base64แบบ JSON
- ส่งเป็น FormData จะไปที่
- นำผลลัพธ์ที่ได้กลับมาตั้งเป็นค่าของ field นั้นในฟอร์ม
- เมื่อผู้ใช้กดส่งฟอร์ม ขั้นตอน normalize ค่าจะแปลงค่าที่เก็บไว้ให้เหลือเฉพาะ path หรือชื่อไฟล์ก่อนส่งจริง
- ผู้ใช้สามารถลบไฟล์ที่แนบไว้ได้ ซึ่งจะเคลียร์ค่าของ field กลับเป็นค่าว่าง
- ในฝั่งบอร์ดประกาศ รูปที่อัปโหลดสำเร็จจะถูกเก็บเป็นรายการ URL แล้วส่งไปพร้อม payload ของโพสต์ โดยมีเพดานจำนวนรูปต่อโพสต์และต่อคอมเมนต์ตามการตั้งค่าของ OA นั้น
หน้าจอและองค์ประกอบหลัก
Service กลาง
- Upload service (
src/service/upload-file.service.ts) เป็นจุดเดียวที่ติดต่อกับ endpoint อัปโหลด รองรับทั้งการส่งแบบ multipart และแบบ base64
คอมโพเนนต์ฝั่งฟอร์ม
- คอมโพเนนต์อัปโหลดหลัก (
src/components/form-builder/upload/upload-1.component.tsx) จัดการการเรียก API การลบไฟล์ และการแสดงชื่อไฟล์ที่อัปโหลดสำเร็จแล้ว - คอมโพเนนต์อัปโหลดรุ่นเดิม (
src/components/form-builder/file-upload.component.tsx) ยังคงอยู่ในโปรเจกต์เพื่อรองรับฟอร์มที่กำหนดค่าไว้แบบเก่า
คอมโพเนนต์ฝั่งบอร์ดประกาศ (อยู่ภายใต้ src/app/[hash]/bulletin/components/)
- แผ่นเขียนโพสต์และช่องเขียนคอมเมนต์เรียก upload service โดยตรงเพื่อแนบรูป
- คอมโพเนนต์แสดงรูปแบบตารางรับผิดชอบการแสดงรูปที่แนบมาแล้ว
Endpoint ที่ใช้
| Method | Path | รูปแบบข้อมูล |
|---|---|---|
| POST | /upload-file/temp | multipart/form-data |
| POST | /upload-file/temp-base64 | application/json |
จุดเชื่อมต่อกับฟีเจอร์อื่น
- Ant Design Upload และ Dragger เป็น UI พื้นฐานของการอัปโหลดในฝั่งฟอร์ม
- การตั้งค่านามสกุลไฟล์ที่ยอมรับกำหนดมาจากนิยาม field ในฝั่ง CMS และถูกส่งมาพร้อมโครงฟอร์ม
- ถูกใช้โดย กรอกฟอร์ม, บอร์ดประกาศ และ รายละเอียดประกาศ
- ไฟล์ที่อัปโหลดผ่าน endpoint เหล่านี้มีสถานะเป็นไฟล์ชั่วคราว การผูกเข้ากับ record จริงเป็นความรับผิดชอบของฝั่ง API ในขั้นตอนบันทึกข้อมูล
รายละเอียดฝั่ง Backend (Client API)
บริการอัปโหลดฝั่ง backend ไม่แตะฐานข้อมูลเลย ทำงานกับ object storage อย่างเดียว และ ทั้งสอง endpoint เป็นเส้นทางสาธารณะที่ไม่ต้องมี LIFF token — จุดนี้สำคัญมากและต้องอ่านคู่กับกลไก commit ตอนบันทึกข้อมูลจริง ซึ่งเป็นตัวพิสูจน์ความเป็นเจ้าของไฟล์
การตรวจไฟล์ตอนอัปโหลดแบบ multipart (POST /api/upload-file/temp)
- ไม่มี field
fileหรืออ่านไม่ได้ → 400file must be a file - ขนาดเกิน 10 MB → 400 พร้อมข้อความที่ระบุขนาดสูงสุดเป็นจำนวน byte ตรงๆ
- backend ตรวจชนิดไฟล์จากเนื้อไฟล์จริง ไม่เชื่อ
Content-Typeที่ browser ส่งมา — การเปลี่ยนนามสกุลไฟล์หรือปลอม header จึงไม่ช่วยให้ผ่าน - ชนิดที่อนุญาตมีเพียง JPEG, PNG และ PDF เท่านั้น นอกนั้น → 400
file has invalid mime type - เขียนไฟล์ลงโฟลเดอร์ชั่วคราวด้วยชื่อรูปแบบ
temp/temp-{เวลาเป็นมิลลิวินาที}.{นามสกุล}แล้วคืน public URL กลับมา
ข้อควรระวังสำหรับหน้าเว็บ: รายการนามสกุลไฟล์ที่ยอมรับซึ่งตั้งค่าไว้ในนิยาม field ฝั่ง CMS เป็นเพียง UI hint เท่านั้น ด่านจริงคือชุดชนิดที่ backend อนุญาต ถ้าแอดมินตั้งค่าให้รับไฟล์ชนิดอื่น (เช่น docx, xlsx) ผู้ใช้จะเลือกไฟล์ได้แต่จะถูกปฏิเสธตอนอัปโหลด
ความแตกต่างของ endpoint แบบ base64 (POST /api/upload-file/temp-base64)
- ชุดชนิดไฟล์ที่รองรับกว้างกว่า endpoint แบบ multipart คือ jpg, png, gif, pdf และ webp — ตรวจจากลายเซ็นของไฟล์ ถ้าตรวจไม่ได้จะได้ 400 พร้อมข้อความที่ระบุชนิดที่รองรับ
- ไม่มีการจำกัดขนาด 10 MB ในเส้นทางนี้ ต่างจาก endpoint แบบ multipart อย่างชัดเจน
- ระบบตัดส่วนหัวของ data URI ออกให้เอง (ทุกอย่างก่อนเครื่องหมายจุลภาคแรก) จึงส่ง data URI เต็มมาได้
- เนื้อไฟล์ที่ decode แล้วว่างเปล่า → 400
Empty file content - ข้อสังเกต parity: endpoint นี้ยังทำการ copy ไฟล์ไปยัง path หนึ่งที่ ไม่มีใครใช้งานเลย (ค้างมาจากระบบเดิม) — เป็นการ port ไว้โดยเจตนา ค่าที่คืนกลับมายังเป็น URL ของไฟล์ในโฟลเดอร์ชั่วคราว ไม่ใช่ของสำเนานั้น
สิ่งที่เกิดขึ้นหลังจากนั้น (สำคัญกว่าตัว endpoint)
เพราะ endpoint อัปโหลดไม่ต้องยืนยันตัวตน การพิสูจน์ความเป็นเจ้าของไฟล์จึงไปเกิดตอนบันทึกข้อมูลจริง และแต่ละฟีเจอร์ทำไม่เหมือนกัน:
- ฟอร์ม: ตอนส่งฟอร์ม backend จะ copy ไฟล์จากโฟลเดอร์ชั่วคราวไปยังที่เก็บถาวรของฟอร์มนั้น โดยตรวจชื่อไฟล์ให้มีเฉพาะตัวอักษร ตัวเลข จุด ขีดล่างและขีดกลาง — path ที่ไม่ได้อยู่ในโฟลเดอร์ชั่วคราวจะถูกข้ามไปเงียบๆ
- บอร์ดประกาศ: เข้มกว่า โดย ตรวจกับ storage ก่อนว่า object นั้นมีอยู่จริงก่อนจะ copy — นี่คือด่านเดียวที่กันการอ้างถึง object ของ tenant อื่นหรือ key ที่ไม่เคยมีการอัปโหลดจริง และยอมรับได้ทั้ง public URL และ key ดิบที่ตรงรูปแบบชื่อไฟล์ชั่วคราวเท่านั้น
ข้อสังเกตด้าน security ที่ควรรู้
- ทั้งสอง endpoint เปิดสาธารณะ ใครก็อัปโหลดไฟล์เข้าโฟลเดอร์ชั่วคราวได้โดยไม่ต้องมี token — สิ่งที่คุมปริมาณคือ rate limit ระดับแอปตาม IP เท่านั้น (ดู โครงแอปและ Provider หลัก)
- การตรวจชนิดไฟล์จากเนื้อไฟล์จริง (ไม่เชื่อ header ของ client) เป็นการป้องกันการอัปโหลดไฟล์ปลอมนามสกุลได้ตรงจุด
- เส้นทางของฟอร์มไม่ได้พิสูจน์ว่า object มีอยู่จริงก่อน copy เหมือนเส้นทางบอร์ดประกาศ — เป็นความต่างที่ควรรู้ระหว่างสองฟีเจอร์
Edge case ที่ควรรู้
- ไฟล์ในโฟลเดอร์ชั่วคราวไม่เคยถูกลบโดย request ใดเลย ผู้ใช้ที่อัปโหลดแล้วละทิ้งฟอร์ม จะทิ้งไฟล์ค้างไว้ถาวร — การกวาดโฟลเดอร์นี้เป็นงาน operations แยกต่างหาก ไม่ใช่สิ่งที่แอปทำให้
- ชื่อไฟล์ที่เก็บจริงถูกตั้งใหม่จากเวลาเป็นมิลลิวินาที ชื่อไฟล์เดิมของผู้ใช้ไม่ถูกเก็บไว้ ถ้าหน้าเว็บต้องแสดงชื่อไฟล์เดิม ต้องจำไว้ในฝั่ง client เอง
- เพราะชื่อไฟล์อิงเวลาเป็นมิลลิวินาที การอัปโหลดพร้อมกันในมิลลิวินาทีเดียวกันมีโอกาสชนกันได้ในทางทฤษฎี
- ถ้าระบบไม่ได้ตั้งค่า object storage ไว้ การอัปโหลดจะล้มเหลว ไม่มี fallback ให้เก็บที่อื่น