อัปโหลดไฟล์ชั่วคราว (Upload File)
ภาพรวม
บริการอัปโหลดที่ใช้ร่วมกันระหว่างฟอร์ม (คำถามชนิด file_upload) และบอร์ดประกาศ (รูปภาพในโพสต์และคอมเมนต์) ไฟล์จะถูกเก็บลง prefix temp/ ก่อน แล้ว feature ที่เป็นเจ้าของจะย้ายไปยังที่เก็บถาวรตอนบันทึกข้อมูลจริง
feature นี้ ไม่แตะ database เลย และทั้งสอง endpoint เป็น endpoint สาธารณะที่ไม่ต้องใช้ LIFF token จึงต้องอ่านคู่กับกลไก commit ของฝั่งผู้ใช้งาน ซึ่งเป็นตัวพิสูจน์ความเป็นเจ้าของไฟล์ที่แท้จริง
Business Flow
POST /api/upload-file/temp (multipart, field ชื่อ file)
- หากไม่มี field
fileหรืออ่านไม่ได้ จะตอบ 400file must be a file(เทียบเท่า@IsFile()) - ขนาดเกิน 10 MB จะตอบ 400
file must be smaller than or equal to 10485760 bytes(เทียบเท่า@MaxFileSize) - อ่าน buffer แล้ว detect mime จากเนื้อไฟล์จริง ผ่าน
util.DetectExtensionและutil.MIMEForExtโดยไม่เชื่อContent-Typeที่ client ส่งมา ซึ่งเลียนพฤติกรรมของnestjs-form-data - mime ที่อนุญาตมีเพียง
image/jpeg,image/jpg,image/pngและapplication/pdfนอกเหนือจากนี้จะตอบ 400file has invalid mime type PutTempเขียนไฟล์เป็นtemp/temp-{unixMilli}.{ext}แล้วคืน{path: publicURL}
POST /api/upload-file/temp-base64 (JSON ที่มีฟิลด์ file)
- DTO validation กำหนดให้
fileเป็น string ที่ไม่ว่าง (เทียบเท่า@IsStringและ@IsNotEmpty) DetectExtensionFromBase64ตรวจชนิดไฟล์จาก signature หากตรวจไม่ได้จะตอบ 400Unable to detect file type. Supported types: jpg, png, gif, pdf, webpสังเกตว่าชุดชนิดไฟล์ที่รองรับ กว้างกว่า endpoint แบบ multipartcreateFileFromBase64ตัด data-URI prefix ออก (ทุกอย่างก่อนเครื่องหมาย,ตัวแรก) แล้ว base64-decode โดยลองแบบ standard ก่อนแล้วจึง raw หาก decode ไม่ได้จะได้ buffer ว่างและตกไปที่การตรวจถัดไป (ใน source ตัวBuffer.fromของ JS ไม่ throw ทำให้ catch เป็นโค้ดตาย) buffer ว่างจะตอบ 400Empty file contentส่วน mime ที่ map ไม่ได้จะกลายเป็นapplication/octet-streamและoriginalNameถูกตั้งเป็นfile-{unixMilli}PutTempเขียนไฟล์เป็นtemp/temp-{unixMilli}.{ext}แล้วเก็บ public URL ไว้- เรียก
CopyObjectไปยังpublic/mock-line-oa-hash/images/{filename}ซึ่ง port ไว้ตาม source ทั้งที่ปลายทางนี้ไม่มีใครใช้งาน เป็น parity โดยเจตนา - คืน
{path: publicURL}ซึ่งเป็น URL ของไฟล์ในtemp/ไม่ใช่ของสำเนา
สิ่งที่เกิดขึ้นต่อจากนี้
ขั้นตอนหลังการอัปโหลดมีความสำคัญมากกว่าตัว endpoint เอง
- ฟอร์ม: ค่าที่ตอบกลับมาเป็น URL ใน
temp/จากนั้น ส่งคำตอบฟอร์ม จะ copy ไปยังform-builder/{formId}/{filename}ตอน submit โดยตรวจชื่อไฟล์ด้วย pattern^[a-zA-Z0-9._-]+$ - บอร์ดประกาศ: การเขียนโพสต์ใช้ image committer ซึ่งจะเรียก
HeadObjectเพื่อพิสูจน์ว่า object นั้นมีอยู่จริงก่อน copy นับเป็นด่านเดียวที่ป้องกันการอ้างถึง object ของ tenant อื่นหรือ key ที่ไม่เคยถูกอัปโหลด และรองรับทั้ง public URL และ key ดิบที่ตรงกับ^temp/temp-[0-9]+\.[A-Za-z0-9]+$ - object ใน
temp/ไม่เคยถูกลบโดย request ใด การกวาดล้าง prefix นี้เป็นงาน operations ที่แยกออกไปต่างหาก
ไฟล์และฟังก์ชันหลัก
| Route | Handler |
|---|---|
POST /api/upload-file/temp | internal/uploadfile/handler.go → (*Handler).UploadFileTemp |
POST /api/upload-file/temp-base64 | (*Handler).UploadFileTempBase64 |
internal/uploadfile/register.go→Register(r, deps)internal/uploadfile/service.go→NewService,UploadFileTemp,UploadFileTempBase64,createFileFromBase64,nowMilliและ interfacestorageinternal/uploadfile/handler.go→tempBase64DTO,allowedTempMimes,maxTempFileSizeซึ่งกำหนดเป็น10*1024*1024internal/storagex/storagex.go→New(cfg),PutTemp,GetPublicURL,CopyObject,HeadObject,IsFileExistและ typeUploadFileinternal/s3x/s3x.go— client ที่สร้างบน aws-sdk-go-v2internal/util/hash.goและbase64.go→DetectExtension,DetectExtensionFromBase64,MIMEForExt
จุดเชื่อมต่อกับ Service อื่น
- Object storage (S3/MinIO) เพียงอย่างเดียว ไม่ใช้ทั้ง database และ Redis
- Config
deps.Config.Storageซึ่งประกอบด้วย endpoint, bucket และPublicHostที่ถูกใช้ตัด prefix ตอน copy ฝั่งฟอร์ม - ผู้ใช้งานปลายทาง ได้แก่ ส่งคำตอบฟอร์ม รวมถึงการเขียนโพสต์และคอมเมนต์ของบอร์ดประกาศ
- ฝั่ง client-web ที่เกี่ยวข้องคือ feature
file-upload