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 ให้เก็บที่อื่น