บทที่ 6backend|10 KB
บทที่ 6: File Upload System (Multer)
สารบัญเนื้อหา
บทที่ 6: File Upload System (Multer)
🎯 เป้าหมายการเรียนรู้
หลังจากเรียนบทนี้แล้ว คุณจะสามารถ:
- เข้าใจการทำงานของ File Upload
- ใช้ Multer สำหรับจัดการ file uploads
- สร้าง storage configuration
- Validate file types และ sizes
- จัดการ file paths และ URLs
- Handle upload errors
📚 ทฤษฎีและแนวคิด
File Upload คืออะไร?
File Upload คือการส่งไฟล์จาก client ไปยัง server
ประเภทไฟล์ใน QUOM:
- 🎵 Audio: MP3, WAV (บันทึกเสียง)
- 🖼️ Image: PNG, JPG (รูปภาพคำคม)
- 📄 Avatar: PNG, JPG (รูปโปรไฟล์)
Multer คืออะไร?
Multer คือ middleware สำหรับจัดการ multipart/form-data (file uploads)
ข้อดี:
- ✅ ใช้งานง่าย
- ✅ รองรับหลายไฟล์
- ✅ Validate ได้
- ✅ Flexible storage options
💻 โค้ดจริงจาก QUOM App
1. Upload Utility
ไฟล์: backend/src/utils/upload.js
import multer from "multer";
import path from "path";
import fs from "fs";
export function makeUploader(prefix = "audio") {
// 1. Ensure upload directory exists
const dir = process.env.UPLOAD_DIR || "uploads";
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
// 2. Configure storage
const storage = multer.diskStorage({
destination: (req, file, cb) => cb(null, dir),
filename: (req, file, cb) => {
const ext = path.extname(file.originalname || "");
const safe = Date.now() + "-" + Math.random().toString(16).slice(2);
cb(null, `${prefix}-${safe}${ext || ".bin"}`);
}
});
// 3. Create multer instance
return multer({
storage,
limits: { fileSize: 30 * 1024 * 1024 } // 30MB
});
}
อธิบายทีละบรรทัด (Imports):
-
import multer from "multer"- Library ยอดนิยมสำหรับจัดการ File Upload (Multipart/form-data)
-
import path from "path"- Utility สำหรับจัดการ File names และ Extensions (เช่น .jpg, .mp3)
-
import fs from "fs"- File System module ใช้สำหรับสร้าง Folder หรือลบไฟล์
อธิบาย:
1. Directory Setup
const dir = process.env.UPLOAD_DIR || "uploads";
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
- ตรวจสอบว่ามีโฟลเดอร์หรือยัง
- ถ้าไม่มี สร้างใหม่
recursive: true: สร้าง parent directories ด้วย
2. Filename Generation
const ext = path.extname(file.originalname || "");
const safe = Date.now() + "-" + Math.random().toString(16).slice(2);
cb(null, `${prefix}-${safe}${ext || ".bin"}`);
ตัวอย่าง:
- Input:
recording.mp3 - Output:
audio-1705747200000-a3f5b2c1d4.mp3
ทำไมต้องสร้างชื่อใหม่:
- ป้องกันชื่อซ้ำ
- ป้องกัน path traversal attack
- เก็บ extension เดิม
3. File Size Limit
limits: { fileSize: 30 * 1024 * 1024 } // 30MB
- 30 MB = 30 × 1024 × 1024 bytes
- ป้องกัน upload ไฟล์ใหญ่เกินไป
2. Upload Audio Endpoint
ไฟล์: backend/src/routes/notes.js
import { makeUploader } from "../utils/upload.js";
const upload = makeUploader("audio");
notesRouter.post("/:id/upload-audio",
upload.single("audio"),
async (req, res) => {
// 1. Find note
const note = await Note.findOne({
_id: req.params.id,
userId: req.user._id
});
if (!note) {
return res.status(404).json({
ok: false,
message: "Note not found"
});
}
// 2. Build file URL
const base = process.env.BASE_URL ||
`http://localhost:${process.env.PORT || 4010}`;
const url = `${base}/uploads/${req.file.filename}`;
// 3. Save file info to database
note.audio = {
fileName: req.file.filename,
url,
mimeType: req.file.mimetype || "",
durationMs: Number(req.body.durationMs || 0),
};
await note.save();
res.json({ ok: true, note });
}
);
อธิบาย:
1. Multer Middleware
upload.single("audio")
single(): รับ 1 ไฟล์"audio": field name ใน form-data- ไฟล์จะอยู่ใน
req.file
2. File Object
req.file = {
fieldname: 'audio',
originalname: 'recording.mp3',
encoding: '7bit',
mimetype: 'audio/mpeg',
destination: 'uploads/',
filename: 'audio-1705747200000-a3f5b2c1d4.mp3',
path: 'uploads/audio-1705747200000-a3f5b2c1d4.mp3',
size: 1234567
}
3. Build URL
const base = process.env.BASE_URL ||
`http://localhost:${process.env.PORT || 4010}`;
const url = `${base}/uploads/${req.file.filename}`;
ตัวอย่าง:
- Development:
http://localhost:4010/uploads/audio-123.mp3 - Production:
https://api.quom.com/uploads/audio-123.mp3
3. Serve Static Files
ไฟล์: backend/src/server.js
import express from "express";
import path from "path";
const app = express();
// Serve uploaded files
const uploadDir = process.env.UPLOAD_DIR || "uploads";
app.use("/uploads", express.static(uploadDir));
การใช้งาน:
- URL:
GET /uploads/audio-123.mp3 - Express จะส่งไฟล์กลับไป
4. File Validation
const upload = multer({
storage,
limits: {
fileSize: 30 * 1024 * 1024 // 30MB
},
fileFilter: (req, file, cb) => {
// Allow only audio files
const allowedMimes = [
'audio/mpeg',
'audio/mp3',
'audio/wav',
'audio/x-wav'
];
if (allowedMimes.includes(file.mimetype)) {
cb(null, true); // Accept
} else {
cb(new Error('Invalid file type. Only audio files allowed.'));
}
}
});
fileFilter Parameters:
req: Express requestfile: File objectcb: Callback (error, accept)
5. Multiple Files Upload
// Multiple files (same field)
upload.array("photos", 5) // Max 5 files
// Multiple fields
upload.fields([
{ name: 'audio', maxCount: 1 },
{ name: 'image', maxCount: 1 }
])
Usage:
notesRouter.post("/upload-multiple",
upload.array("photos", 5),
async (req, res) => {
// req.files is an array
const files = req.files.map(f => ({
filename: f.filename,
url: `/uploads/${f.filename}`
}));
res.json({ ok: true, files });
}
);
6. Error Handling
notesRouter.post("/:id/upload-audio",
(req, res, next) => {
upload.single("audio")(req, res, (err) => {
if (err instanceof multer.MulterError) {
// Multer errors
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({
ok: false,
message: 'File too large (max 30MB)'
});
}
return res.status(400).json({
ok: false,
message: err.message
});
} else if (err) {
// Other errors (e.g., fileFilter)
return res.status(400).json({
ok: false,
message: err.message
});
}
next();
});
},
async (req, res) => {
// Handle upload
}
);
🔒 Security Best Practices
1. Validate File Types
// ✅ ดี - Whitelist
const allowedMimes = ['audio/mpeg', 'audio/wav'];
if (!allowedMimes.includes(file.mimetype)) {
return cb(new Error('Invalid file type'));
}
// ❌ อันตราย - ไม่ validate
cb(null, true); // รับทุกไฟล์!
2. Limit File Size
// ✅ ดี
limits: { fileSize: 30 * 1024 * 1024 }
// ❌ อันตราย - ไม่จำกัด
limits: { fileSize: Infinity }
3. Sanitize Filenames
// ✅ ดี - สร้างชื่อใหม่
const safe = Date.now() + "-" + Math.random().toString(16).slice(2);
// ❌ อันตราย - ใช้ชื่อเดิม
const filename = file.originalname; // อาจมี ../../../etc/passwd
4. Store Outside Web Root
// ✅ ดี
const dir = "uploads"; // นอก public folder
// ❌ อันตราย
const dir = "public/uploads"; // เข้าถึงได้โดยตรง
📝 แบบฝึกหัด
โจทย์ที่ 1: Image Upload
// TODO: สร้าง image uploader
// - รับเฉพาะ PNG, JPG
// - จำกัดขนาด 5MB
// - Resize เป็น 800x800
โจทย์ที่ 2: Delete File
// TODO: สร้าง endpoint ลบไฟล์
// - ลบจาก database
// - ลบไฟล์จริงด้วย fs.unlink()
โจทย์ที่ 3: Cloud Storage
// TODO: Upload ไป AWS S3
// - ใช้ aws-sdk
// - Return S3 URL
📌 สรุป
✅ Multer - Middleware สำหรับ file uploads
✅ Storage - diskStorage configuration
✅ Validation - File type และ size
✅ Security - Sanitize filenames
✅ Static Files - Serve uploaded files
ไฟล์ที่เกี่ยวข้อง
🔜 บทถัดไป
บทที่ 7: Email Service และ OTP
เรียนรู้การส่ง email และสร้างระบบ OTP