Skip to main content

อัปโหลดไฟล์/รูปภาพ

ภาพรวม​

ระบบมีบริการอัปโหลดไฟล์แบบชั่วคราว (temp) ที่ใช้ร่วมกันระหว่างสองฟีเจอร์หลัก คือ field ชนิดอัปโหลดไฟล์ในฟอร์ม และการแนบรูปภาพในโพสต์หรือคอมเมนต์บนบอร์ดประกาศ

หลักการทำงานคือ ไฟล์จะถูกอัปโหลดขึ้น API ทันทีที่ผู้ใช้เลือกไฟล์ แล้วเก็บเฉพาะ path หรือ URL ที่ได้กลับมาไว้ในค่าของ field หรือใน payload ของโพสต์ การผูกไฟล์ชั่วคราวเข้ากับ record จริงเกิดขึ้นที่ฝั่ง API ในขั้นตอนบันทึกข้อมูล

Business Flow​

  1. ผู้ใช้เลือกไฟล์ ซึ่งอาจมาจากคอมโพเนนต์อัปโหลดของ Ant Design ในฝั่งฟอร์ม หรือจาก file input มาตรฐานในฝั่งบอร์ดประกาศ
  2. คอมโพเนนต์เรียก handler การอัปโหลดของตัวเอง โดยไม่ใช้กลไกอัปโหลดเริ่มต้นของ Ant Design เพื่อควบคุมการเรียก API และการจัดการข้อผิดพลาดได้เอง
  3. ระบบประกอบข้อมูลแล้วเรียก upload service ซึ่งเลือกปลายทางตามรูปแบบข้อมูลที่ส่ง
    • ส่งเป็น FormData จะไปที่ POST /upload-file/temp แบบ multipart
    • ส่งเป็นสตริง base64 จะไปที่ POST /upload-file/temp-base64 แบบ JSON
  4. นำผลลัพธ์ที่ได้กลับมาตั้งเป็นค่าของ field นั้นในฟอร์ม
  5. เมื่อผู้ใช้กดส่งฟอร์ม ขั้นตอน normalize ค่าจะแปลงค่าที่เก็บไว้ให้เหลือเฉพาะ path หรือชื่อไฟล์ก่อนส่งจริง
  6. ผู้ใช้สามารถลบไฟล์ที่แนบไว้ได้ ซึ่งจะเคลียร์ค่าของ field กลับเป็นค่าว่าง
  7. ในฝั่งบอร์ดประกาศ รูปที่อัปโหลดสำเร็จจะถูกเก็บเป็นรายการ 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 ที่ใช้

MethodPathรูปแบบข้อมูล
POST/upload-file/tempmultipart/form-data
POST/upload-file/temp-base64application/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)​

  1. ไม่มี field file หรืออ่านไม่ได้ → 400 file must be a file
  2. ขนาดเกิน 10 MB → 400 พร้อมข้อความที่ระบุขนาดสูงสุดเป็นจำนวน byte ตรงๆ
  3. backend ตรวจชนิดไฟล์จากเนื้อไฟล์จริง ไม่เชื่อ Content-Type ที่ browser ส่งมา — การเปลี่ยนนามสกุลไฟล์หรือปลอม header จึงไม่ช่วยให้ผ่าน
  4. ชนิดที่อนุญาตมีเพียง JPEG, PNG และ PDF เท่านั้น นอกนั้น → 400 file has invalid mime type
  5. เขียนไฟล์ลงโฟลเดอร์ชั่วคราวด้วยชื่อรูปแบบ 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 ให้เก็บที่อื่น