Back

Multer NPM : Téléchargement de fichiers dans Node.js

Multer NPM : Téléchargement de fichiers dans Node.js

Le téléchargement de fichiers est une exigence courante dans les applications web, mais les gérer correctement dans Node.js peut être difficile. Multer est un middleware pour Express.js qui rend le téléchargement de fichiers simple et efficace. Cet article explore comment utiliser Multer pour gérer les téléchargements de fichiers dans vos applications Node.js.

Points clés à retenir

  • Multer simplifie le téléchargement de fichiers dans Node.js en gérant multipart/form-data
  • Configurez les options de stockage selon vos besoins (disque, mémoire ou cloud)
  • Implémentez une validation et un filtrage appropriés pour assurer la sécurité
  • Définissez des limites appropriées pour prévenir les abus
  • Gérez les erreurs avec élégance pour offrir une bonne expérience utilisateur
  • Envisagez le stockage cloud pour les applications en production

Qu’est-ce que Multer ?

Multer est un middleware Node.js spécifiquement conçu pour gérer multipart/form-data, qui est le format utilisé lors du téléchargement de fichiers via des formulaires HTML. Construit sur busboy, Multer traite efficacement les téléchargements de fichiers et les rend accessibles dans vos routes Express.

Les principales fonctionnalités de Multer comprennent :

  • Gestion des téléchargements de fichiers uniques et multiples
  • Options de stockage configurables
  • Capacités de filtrage de fichiers
  • Limites de taille et validation
  • Intégration transparente avec Express.js

Comprendre les données de formulaire multipart

Avant de plonger dans Multer, il est important de comprendre pourquoi le traitement de formulaire régulier ne fonctionne pas pour les téléchargements de fichiers.

Lorsqu’un formulaire contient des fichiers, le navigateur l’encode en multipart/form-data au lieu du application/x-www-form-urlencoded par défaut. Cet encodage permet aux données binaires (fichiers) d’être transmises aux côtés des champs texte.

Les middlewares intégrés d’Express (express.json() et express.urlencoded()) ne peuvent pas traiter les données multipart, c’est pourquoi nous avons besoin de Multer.

<!-- Ce formulaire enverra multipart/form-data -->
<form action=""/upload"" method=""POST"" enctype=""multipart/form-data"">
  <input type=""file"" name=""document"">
  <button type=""submit"">Upload</button>
</form>

Démarrer avec Multer

Installation

D’abord, installez Multer dans votre projet Node.js :

npm install multer

Configuration de base

Voici un exemple simple de configuration de Multer dans une application Express :

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

const app = express();
const upload = multer({ dest: 'uploads/' });

app.post('/upload', upload.single('document'), (req, res) => {
  // req.file contient des informations sur le fichier téléchargé
  console.log(req.file);
  
  // req.body contient tous les champs texte
  console.log(req.body);
  
  res.send('File uploaded successfully');
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

Dans cet exemple :

  1. Nous créons une instance Multer avec un dossier de destination pour les téléchargements
  2. Nous utilisons le middleware upload.single('document') pour gérer un téléchargement de fichier unique
  3. Les informations sur le fichier téléchargé sont disponibles dans req.file

Gestion de différents scénarios de téléchargement

Multer fournit plusieurs méthodes pour gérer différents scénarios de téléchargement de fichiers :

Téléchargement d’un seul fichier

// Gérer un seul fichier avec le nom de champ 'profile'
app.post('/profile', upload.single('profile'), (req, res) => {
  // req.file contient le fichier téléchargé
  res.json({ 
    message: 'File uploaded successfully',
    file: req.file 
  });
});

Fichiers multiples (même champ)

// Gérer plusieurs fichiers (max 5) avec le nom de champ 'photos'
app.post('/photos', upload.array('photos', 5), (req, res) => {
  // req.files contient un tableau de fichiers
  res.json({ 
    message: `${req.files.length} files uploaded successfully`,
    files: req.files 
  });
});

Fichiers multiples (champs différents)

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

app.post('/profile', uploadFields, (req, res) => {
  // req.files est un objet avec les noms de champ comme clés
  // req.files.avatar contient le fichier avatar
  // req.files.gallery contient les fichiers de la galerie
  res.json({ 
    message: 'Files uploaded successfully',
    avatar: req.files.avatar,
    gallery: req.files.gallery
  });
});

Formulaires multipart texte uniquement

app.post('/form-data', upload.none(), (req, res) => {
  // Traite uniquement les champs texte, rejette tous les fichiers
  res.json(req.body);
});

Options de stockage

Multer fournit différents moteurs de stockage pour contrôler où et comment les fichiers sont stockés.

Stockage sur disque

Pour plus de contrôle sur le stockage des fichiers, utilisez diskStorage :

const storage = multer.diskStorage({
  destination: function(req, file, cb) {
    cb(null, 'uploads/');
  },
  filename: function(req, file, cb) {
    // Créer un nom de fichier unique
    const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
    cb(null, file.fieldname + '-' + uniqueSuffix + getExtension(file.originalname));
  }
});

function getExtension(filename) {
  return filename.substring(filename.lastIndexOf('.'));
}

const upload = multer({ storage: storage });

Cette configuration vous donne le contrôle sur :

  • Où les fichiers sont stockés (destination)
  • Comment ils sont nommés (filename)

Stockage en mémoire

Si vous devez traiter des fichiers en mémoire sans les enregistrer sur le disque :

const storage = multer.memoryStorage();
const upload = multer({ storage: storage });

app.post('/upload', upload.single('file'), (req, res) => {
  // req.file.buffer contient les données du fichier
  // Traiter le fichier en mémoire (par exemple, télécharger vers un stockage cloud)
  
  // Exemple : Obtenir le buffer du fichier et son type
  const fileBuffer = req.file.buffer;
  const fileType = req.file.mimetype;
  
  res.send('File processed');
});

Le stockage en mémoire est utile quand :

  • Vous transmettez des fichiers à un autre service (comme S3 ou Cloudinary)
  • Vous devez traiter le fichier avant de l’enregistrer
  • Vous travaillez dans des environnements sans serveur

Filtrage de fichiers

Vous pouvez contrôler quels fichiers sont acceptés en utilisant l’option fileFilter :

const fileFilter = (req, file, cb) => {
  // Accepter uniquement les fichiers image
  if (file.mimetype.startsWith('image/')) {
    cb(null, true);
  } else {
    cb(new Error('Only image files are allowed!'), false);
  }
};

const upload = multer({ 
  storage: multer.diskStorage({...}),
  fileFilter: fileFilter
});

Limites de taille et sécurité

Définir des limites est crucial pour la sécurité et les performances :

const upload = multer({
  storage: multer.diskStorage({...}),
  limits: {
    fileSize: 5 * 1024 * 1024, // 5MB en octets
    files: 5 // Maximum 5 fichiers par requête
  }
});

Les autres options de limite incluent :

  • fieldNameSize : Taille maximale du nom de champ (par défaut 100 octets)
  • fieldSize : Taille maximale de la valeur du champ (par défaut 1MB)
  • fields : Nombre maximal de champs non-fichiers (par défaut Infinity)
  • parts : Nombre maximal de parties (champs + fichiers) (par défaut Infinity)

Gestion des erreurs

Les erreurs Multer doivent être gérées correctement pour fournir un retour significatif :

app.post('/upload', (req, res) => {
  upload.single('file')(req, res, (err) => {
    if (err instanceof multer.MulterError) {
      // Une erreur Multer s'est produite
      if (err.code === 'LIMIT_FILE_SIZE') {
        return res.status(400).json({ error: 'File too large' });
      }
      return res.status(400).json({ error: err.message });
    } else if (err) {
      // Une erreur inconnue s'est produite
      return res.status(500).json({ error: 'Server error' });
    }
    
    // Tout s'est bien passé
    res.json({ message: 'File uploaded successfully', file: req.file });
  });
});

Exemple complet : Construction d’un système de téléchargement de fichiers

Construisons un système complet de téléchargement de fichiers avec frontend et backend :

Backend (server.js)

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

const app = express();
const port = 3000;

// Servir des fichiers statiques
app.use(express.static('public'));

// Configurer le stockage
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    const uploadDir = 'uploads';
    // Créer le répertoire s'il n'existe pas
    if (!fs.existsSync(uploadDir)) {
      fs.mkdirSync(uploadDir);
    }
    cb(null, uploadDir);
  },
  filename: (req, file, cb) => {
    const uniqueName = `${Date.now()}-${Math.round(Math.random() * 1E9)}${path.extname(file.originalname)}`;
    cb(null, uniqueName);
  }
});

// Configurer le filtre de fichiers
const fileFilter = (req, file, cb) => {
  // Accepter les images et les PDF
  const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
  
  if (allowedTypes.includes(file.mimetype)) {
    cb(null, true);
  } else {
    cb(new Error('Invalid file type. Only JPEG, PNG, GIF, and PDF are allowed.'), false);
  }
};

// Configurer multer
const upload = multer({
  storage: storage,
  fileFilter: fileFilter,
  limits: {
    fileSize: 10 * 1024 * 1024, // 10MB
    files: 5
  }
});

// Route pour télécharger un seul fichier
app.post('/upload/single', (req, res) => {
  upload.single('file')(req, res, (err) => {
    if (err instanceof multer.MulterError) {
      return res.status(400).json({ success: false, message: err.message });
    } else if (err) {
      return res.status(400).json({ success: false, message: err.message });
    }
    
    if (!req.file) {
      return res.status(400).json({ success: false, message: 'No file provided' });
    }
    
    res.json({
      success: true,
      message: 'File uploaded successfully',
      file: {
        filename: req.file.filename,
        originalname: req.file.originalname,
        mimetype: req.file.mimetype,
        size: req.file.size,
        path: req.file.path
      }
    });
  });
});

// Route pour télécharger plusieurs fichiers
app.post('/upload/multiple', (req, res) => {
  upload.array('files', 5)(req, res, (err) => {
    if (err instanceof multer.MulterError) {
      return res.status(400).json({ success: false, message: err.message });
    } else if (err) {
      return res.status(400).json({ success: false, message: err.message });
    }
    
    if (!req.files || req.files.length === 0) {
      return res.status(400).json({ success: false, message: 'No files provided' });
    }
    
    const fileDetails = req.files.map(file => ({
      filename: file.filename,
      originalname: file.originalname,
      mimetype: file.mimetype,
      size: file.size,
      path: file.path
    }));
    
    res.json({
      success: true,
      message: `${req.files.length} files uploaded successfully`,
      files: fileDetails
    });
  });
});

// Obtenir la liste des fichiers téléchargés
app.get('/files', (req, res) => {
  const uploadDir = 'uploads';
  
  fs.readdir(uploadDir, (err, files) => {
    if (err) {
      return res.status(500).json({ success: false, message: 'Error reading files directory' });
    }
    
    res.json({
      success: true,
      files: files
    });
  });
});

app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});

Frontend (public/index.html)

<!DOCTYPE html>
<html lang=""en"">
<head>
  <meta charset=""UTF-8"">
  <meta name=""viewport"" content=""width=device-width, initial-scale=1.0"">
  <title>File Upload with Multer</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      max-width: 800px;
      margin: 0 auto;
      padding: 20px;
    }
    .upload-section {
      margin-bottom: 30px;
      padding: 20px;
      border: 1px solid #ddd;
      border-radius: 5px;
    }
    .file-list {
      margin-top: 20px;
    }
    .file-item {
      padding: 10px;
      margin: 5px 0;
      background-color: #f5f5f5;
      border-radius: 3px;
    }
    .error {
      color: red;
      margin-top: 10px;
    }
    .success {
      color: green;
      margin-top: 10px;
    }
    progress {
      width: 100%;
      margin-top: 10px;
    }
  </style>
</head>
<body>
  <h1>File Upload with Multer</h1>
  
  <div class=""upload-section"">
    <h2>Single File Upload</h2>
    <form id=""singleUploadForm"">
      <input type=""file"" id=""singleFile"" name=""file"" required>
      <button type=""submit"">Upload File</button>
    </form>
    <div id=""singleUploadResult""></div>
    <progress id=""singleUploadProgress"" value=""0"" max=""100"" style=""display:none;""></progress>
  </div>
  
  <div class=""upload-section"">
    <h2>Multiple File Upload</h2>
    <form id=""multipleUploadForm"">
      <input type=""file"" id=""multipleFiles"" name=""files"" multiple required>
      <button type=""submit"">Upload Files</button>
    </form>
    <div id=""multipleUploadResult""></div>
    <progress id=""multipleUploadProgress"" value=""0"" max=""100"" style=""display:none;""></progress>
  </div>
  
  <div class=""upload-section"">
    <h2>Uploaded Files</h2>
    <button id=""refreshFiles"">Refresh List</button>
    <div id=""fileList"" class=""file-list""></div>
  </div>

  <script>
    document.addEventListener('DOMContentLoaded', function() {
      // Single file upload
      const singleUploadForm = document.getElementById('singleUploadForm');
      const singleUploadResult = document.getElementById('singleUploadResult');
      const singleUploadProgress = document.getElementById('singleUploadProgress');
      
      singleUploadForm.addEventListener('submit', function(e) {
        e.preventDefault();
        const fileInput = document.getElementById('singleFile');
        
        if (!fileInput.files.length) {
          singleUploadResult.innerHTML = '<div class=""error"">Please select a file</div>';
          return;
        }
        
        const formData = new FormData();
        formData.append('file', fileInput.files[0]);
        
        singleUploadProgress.style.display = 'block';
        singleUploadProgress.value = 0;
        singleUploadResult.innerHTML = '';
        
        const xhr = new XMLHttpRequest();
        
        xhr.upload.addEventListener('progress', function(e) {
          if (e.lengthComputable) {
            const percentComplete = (e.loaded / e.total) * 100;
            singleUploadProgress.value = percentComplete;
          }
        });
        
        xhr.onload = function() {
          if (xhr.status === 200) {
            const response = JSON.parse(xhr.responseText);
            singleUploadResult.innerHTML = `<div class=""success"">${response.message}</div>`;
            singleUploadForm.reset();
            loadFiles();
          } else {
            let errorMessage = 'Upload failed';
            try {
              const response = JSON.parse(xhr.responseText);
              errorMessage = response.message || errorMessage;
            } catch (e) {}
            singleUploadResult.innerHTML = `<div class=""error"">${errorMessage}</div>`;
          }
          singleUploadProgress.style.display = 'none';
        };
        
        xhr.onerror = function() {
          singleUploadResult.innerHTML = '<div class=""error"">Network error occurred</div>';
          singleUploadProgress.style.display = 'none';
        };
        
        xhr.open('POST', '/upload/single', true);
        xhr.send(formData);
      });
      
      // Multiple file upload
      const multipleUploadForm = document.getElementById('multipleUploadForm');
      const multipleUploadResult = document.getElementById('multipleUploadResult');
      const multipleUploadProgress = document.getElementById('multipleUploadProgress');
      
      multipleUploadForm.addEventListener('submit', function(e) {
        e.preventDefault();
        const fileInput = document.getElementById('multipleFiles');
        
        if (!fileInput.files.length) {
          multipleUploadResult.innerHTML = '<div class=""error"">Please select at least one file</div>';
          return;
        }
        
        const formData = new FormData();
        for (let i = 0; i < fileInput.files.length; i++) {
          formData.append('files', fileInput.files[i]);
        }
        
        multipleUploadProgress.style.display = 'block';
        multipleUploadProgress.value = 0;
        multipleUploadResult.innerHTML = '';
        
        const xhr = new XMLHttpRequest();
        
        xhr.upload.addEventListener('progress', function(e) {
          if (e.lengthComputable) {
            const percentComplete = (e.loaded / e.total) * 100;
            multipleUploadProgress.value = percentComplete;
          }
        });
        
        xhr.onload = function() {
          if (xhr.status === 200) {
            const response = JSON.parse(xhr.responseText);
            multipleUploadResult.innerHTML = `<div class=""success"">${response.message}</div>`;
            multipleUploadForm.reset();
            loadFiles();
          } else {
            let errorMessage = 'Upload failed';
            try {
              const response = JSON.parse(xhr.responseText);
              errorMessage = response.message || errorMessage;
            } catch (e) {}
            multipleUploadResult.innerHTML = `<div class=""error"">${errorMessage}</div>`;
          }
          multipleUploadProgress.style.display = 'none';
        };
        
        xhr.onerror = function() {
          multipleUploadResult.innerHTML = '<div class=""error"">Network error occurred</div>';
          multipleUploadProgress.style.display = 'none';
        };
        
        xhr.open('POST', '/upload/multiple', true);
        xhr.send(formData);
      });
      
      // Load files
      const fileList = document.getElementById('fileList');
      const refreshFilesButton = document.getElementById('refreshFiles');
      
      function loadFiles() {
        fetch('/files')
          .then(response => response.json())
          .then(data => {
            if (data.success) {
              if (data.files.length === 0) {
                fileList.innerHTML = '<p>No files uploaded yet</p>';
              } else {
                let html = '';
                data.files.forEach(file => {
                  html += `<div class=""file-item"">${file}</div>`;
                });
                fileList.innerHTML = html;
              }
            } else {
              fileList.innerHTML = `<div class=""error"">${data.message}</div>`;
            }
          })
          .catch(error => {
            fileList.innerHTML = '<div class=""error"">Error loading files</div>';
          });
      }
      
      refreshFilesButton.addEventListener('click', loadFiles);
      
      // Initial load
      loadFiles();
    });
  </script>
</body>
</html>

Intégration avec le stockage cloud

Pour les applications en production, vous voulez souvent stocker des fichiers dans un stockage cloud plutôt que sur votre serveur. Voici comment intégrer Multer avec AWS S3 :

const AWS = require('aws-sdk');
const multer = require('multer');
const multerS3 = require('multer-s3');

// Configurer AWS
AWS.config.update({
  accessKeyId: process.env.AWS_ACCESS_KEY_ID,
  secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
  region: process.env.AWS_REGION
});

const s3 = new AWS.S3();

// Configurer multer pour utiliser S3
const upload = multer({
  storage: multerS3({
    s3: s3,
    bucket: process.env.S3_BUCKET_NAME,
    metadata: function (req, file, cb) {
      cb(null, { fieldName: file.fieldname });
    },
    key: function (req, file, cb) {
      const uniqueName = `${Date.now()}-${Math.round(Math.random() * 1E9)}`;
      cb(null, uniqueName);
    }
  }),
  limits: {
    fileSize: 5 * 1024 * 1024 // 5MB
  }
});

app.post('/upload', upload.single('file'), (req, res) => {
  res.json({
    success: true,
    message: 'File uploaded to S3 successfully',
    fileLocation: req.file.location // URL S3
  });
});

Note : Vous devrez installer le package multer-s3 : npm install multer-s3 aws-sdk.

Bonnes pratiques de sécurité

Lors de l’implémentation de téléchargements de fichiers, suivez ces bonnes pratiques de sécurité :

  1. Valider les types de fichiers : Vérifiez toujours les types MIME et les extensions
  2. Limiter les tailles de fichiers : Prévenez les attaques DoS par des téléchargements de fichiers volumineux
  3. Scanner les logiciels malveillants : Envisagez d’intégrer une analyse antivirus pour les fichiers téléchargés
  4. Utiliser un stockage sécurisé : Ne stockez pas les fichiers dans des répertoires publics
  5. Générer des noms de fichiers aléatoires : N’utilisez pas directement les noms de fichiers fournis par l’utilisateur
  6. Implémenter l’authentification : Assurez-vous que seuls les utilisateurs autorisés peuvent télécharger des fichiers
  7. Définir des limites de téléchargement : Limitez le nombre de fichiers par requête et par utilisateur

Conclusion

Multer fournit une solution robuste pour gérer les téléchargements de fichiers dans les applications Node.js. En suivant les modèles et pratiques décrits dans cet article, vous pouvez implémenter une fonctionnalité de téléchargement de fichiers sécurisée, efficace et conviviale. Que vous construisiez un simple téléchargeur d’images ou un système complexe de gestion de documents, l’API flexible de Multer et ses capacités d’intégration en font un excellent choix pour gérer les données multipart/form-data dans vos applications Express.

FAQ

Pour les fichiers volumineux, envisagez : d'augmenter la limite de taille dans la configuration de Multer, d'implémenter des téléchargements par morceaux pour les très gros fichiers, d'utiliser le streaming pour traiter les fichiers sans les charger entièrement en mémoire, et d'implémenter un suivi de progression sur le frontend.

Oui, Multer fonctionne bien avec TypeScript. Installez les définitions de type avec : npm install @types/multer

Listen to your bugs 🧘, with OpenReplay

See how users use your app and resolve issues fast.
Loved by thousands of developers