Kodax

File Upload — REST APIs

رفع الملفات (File Upload)

رفع الملفات من أخطر نقاط أي تطبيق: ملف ضخم يملأ القرص، أو "صورة" هي في الحقيقة سكربت، أو اسم ملف ../../server.js.

كيف يصل الملف؟

النموذج أو الطلب يُرسل بصيغة multipart/form-data (لا JSON):

<form action="/api/avatar" method="post" enctype="multipart/form-data">
    <input type="file" name="avatar" accept="image/png,image/jpeg">
    <button type="submit">رفع</button>
</form>

express.json() لا يقرأ هذه الصيغة؛ نحتاج multer (أو busboy).

multer مع حدود

const multer = require("multer");

const upload = multer({
  storage: multer.memoryStorage(),                 // أو diskStorage
  limits: { fileSize: 2 * 1024 * 1024, files: 1 }, // 2MB، ملف واحد
  fileFilter: (req, file, cb) => {
    const ok = ["image/png", "image/jpeg", "image/webp"].includes(file.mimetype);
    cb(ok ? null : new Error("نوع غير مسموح"), ok);
  },
});
// app.post("/api/avatar", requireAuth, upload.single("avatar"), handler)

بلا limits يستطيع أي شخص رفع ملف بحجم القرص كله.

لا تثق بالنوع ولا بالاسم

- file.mimetype والامتداد يرسلهما العميل ويمكن تزويرهما. افحص التوقيع الحقيقي للملف (البايتات الأولى: PNG يبدأ بـ 89 50 4E 47) بمكتبة مثل file-type، أو أعد معالجة الصورة بمكتبة صور (sharp) — يزيل أيضاً البيانات الوصفية مثل موقع GPS.

- لا تستخدم اسم الملف الأصلي للحفظ: ولّد اسماً عشوائياً (crypto.randomUUID() + ".png") وخزّن الاسم الأصلي في قاعدة البيانات إن احتجته.

- لا تحفظ الملفات المرفوعة داخل مجلد يُنفَّذ أو يُقدَّم كصفحات (مثل public مع HTML). ملف .html مرفوع ومقدَّم من نطاقك = XSS.

أين تُخزَّن؟

| الخيار | ملاحظات |

|---|---|

| قرص الخادم | بسيط، لكنه يضيع مع إعادة النشر على أغلب المنصات الحديثة ولا يتوسع لعدة خوادم |

| تخزين سحابي (S3، Cloudflare R2، Supabase Storage، Firebase Storage) | الحل المعتاد؛ والأفضل: الخادم يصدر رابط رفع موقّع قصير المدة، والعميل يرفع مباشرة دون أن يمر الملف بخادمك |

معالجة أخطاء multer

تجاوز الحد يرمي multer.MulterError برمز مثل LIMIT_FILE_SIZE؛ حوّله في معالج الأخطاء إلى 413 أو 400 برسالة واضحة.

تمرين

أضف إلى المثال الكامل فحص توقيع الملف (أول 4 بايتات) لـ PNG و JPEG من req.file.buffer، وارفض الملف إذا لم يطابق ادعاء mimetype.

// npm install multer

const multer = require('multer');
const path = require('path');

// ===== Disk Storage =====
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, 'uploads/');
  },
  filename: (req, file, cb) => {
    // unique filename: user-123-1699999999.jpg
    const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
    const ext = path.extname(file.originalname);
    cb(null, `user-${req.user.id}-${uniqueSuffix}${ext}`);
  }
});

// ===== File Filter =====
const fileFilter = (req, file, cb) => {
  // قبول الصور فقط
  if (file.mimetype.startsWith('image/')) {
    cb(null, true);
  } else {
    cb(new Error('يرجى رفع صورة فقط'), false);
  }
};

// ===== Multer Configuration =====
const upload = multer({
  storage: storage,
  fileFilter: fileFilter,
  limits: {
    fileSize: 5 * 1024 * 1024  // 5MB max
  }
});

// ===== Single File Upload =====
app.post('/api/upload/avatar', 
  upload.single('avatar'),  // 'avatar' = field name
  (req, res) => {
    if (!req.file) {
      return res.status(400).json({ error: 'لم يتم رفع ملف' });
    }
    
    res.json({
      success: true,
      file: {
        filename: req.file.filename,
        path: req.file.path,
        size: req.file.size,
        mimetype: req.file.mimetype
      }
    });
  }
);

// ===== Multiple Files =====
app.post('/api/upload/photos',
  upload.array('photos', 5),  // max 5 files
  (req, res) => {
    res.json({
      success: true,
      files: req.files.map(f => ({
        filename: f.filename,
        size: f.size
      }))
    });
  }
);

// ===== Mixed Fields =====
const uploadFields = upload.fields([
  { name: 'avatar', maxCount: 1 },
  { name: 'gallery', maxCount: 5 }
]);

app.post('/api/upload/profile', uploadFields, (req, res) => {
  const avatar = req.files['avatar'][0];
  const gallery = req.files['gallery'];
  // ...
});

// ===== Error Handling =====
app.use((err, req, res, next) => {
  if (err instanceof multer.MulterError) {
    if (err.code === 'LIMIT_FILE_SIZE') {
      return res.status(400).json({ error: 'الملف كبير جداً (الحد 5MB)' });
    }
  }
  next(err);
});
اختبر فهمك