Back

Multer NPM: Загрузка файлов в Node.js

Multer NPM: Загрузка файлов в Node.js

Загрузка файлов — это распространенное требование в веб-приложениях, но правильная обработка их в Node.js может быть сложной задачей. Multer — это промежуточное ПО (middleware) для Express.js, которое делает загрузку файлов простой и эффективной. В этой статье рассматривается, как использовать Multer для обработки загрузки файлов в ваших приложениях Node.js.

Ключевые моменты

  • Multer упрощает загрузку файлов в Node.js, обрабатывая multipart/form-data
  • Настраивайте параметры хранения в зависимости от ваших потребностей (диск, память или облако)
  • Реализуйте правильную валидацию и фильтрацию для обеспечения безопасности
  • Устанавливайте соответствующие ограничения для предотвращения злоупотреблений
  • Корректно обрабатывайте ошибки для обеспечения хорошего пользовательского опыта
  • Рассмотрите возможность использования облачного хранилища для продакшн-приложений

Что такое Multer?

Multer — это промежуточное ПО для Node.js, специально разработанное для обработки multipart/form-data, формата, используемого при загрузке файлов через HTML-формы. Построенный на основе busboy, Multer эффективно обрабатывает загрузку файлов и делает их доступными в ваших маршрутах Express.

Ключевые особенности Multer включают:

  • Обработку одиночных и множественных загрузок файлов
  • Настраиваемые параметры хранения
  • Возможности фильтрации файлов
  • Ограничения размера и валидацию
  • Бесшовную интеграцию с Express.js

Понимание Multipart Form Data

Прежде чем погрузиться в Multer, важно понять, почему обычная обработка форм не работает для загрузки файлов.

Когда форма содержит файлы, браузер кодирует её как multipart/form-data вместо стандартного application/x-www-form-urlencoded. Это кодирование позволяет передавать бинарные данные (файлы) вместе с текстовыми полями.

Встроенное промежуточное ПО Express (express.json() и express.urlencoded()) не может обрабатывать данные в формате multipart, поэтому нам нужен Multer.

<!-- Эта форма будет отправлять multipart/form-data -->
<form action=""/upload"" method=""POST"" enctype=""multipart/form-data"">
  <input type=""file"" name=""document"">
  <button type=""submit"">Upload</button>
</form>

Начало работы с Multer

Установка

Сначала установите Multer в ваш проект Node.js:

npm install multer

Базовая настройка

Вот простой пример настройки Multer в приложении 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 содержит информацию о загруженном файле
  console.log(req.file);
  
  // req.body содержит любые текстовые поля
  console.log(req.body);
  
  res.send('File uploaded successfully');
});

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

В этом примере:

  1. Мы создаем экземпляр Multer с папкой назначения для загрузок
  2. Мы используем промежуточное ПО upload.single('document') для обработки одиночной загрузки файла
  3. Информация о загруженном файле доступна в req.file

Обработка различных сценариев загрузки

Multer предоставляет несколько методов для обработки различных сценариев загрузки файлов:

Загрузка одного файла

// Обработка одного файла с именем поля 'profile'
app.post('/profile', upload.single('profile'), (req, res) => {
  // req.file содержит загруженный файл
  res.json({ 
    message: 'File uploaded successfully',
    file: req.file 
  });
});

Несколько файлов (одно поле)

// Обработка нескольких файлов (максимум 5) с именем поля 'photos'
app.post('/photos', upload.array('photos', 5), (req, res) => {
  // req.files содержит массив файлов
  res.json({ 
    message: `${req.files.length} files uploaded successfully`,
    files: req.files 
  });
});

Несколько файлов (разные поля)

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

app.post('/profile', uploadFields, (req, res) => {
  // req.files — это объект с именами полей в качестве ключей
  // req.files.avatar содержит файл аватара
  // req.files.gallery содержит файлы галереи
  res.json({ 
    message: 'Files uploaded successfully',
    avatar: req.files.avatar,
    gallery: req.files.gallery
  });
});

Только текстовые multipart-формы

app.post('/form-data', upload.none(), (req, res) => {
  // Обрабатывать только текстовые поля, отклонять любые файлы
  res.json(req.body);
});

Варианты хранения

Multer предоставляет различные механизмы хранения для контроля над тем, где и как хранятся файлы.

Хранение на диске

Для большего контроля над хранением файлов используйте diskStorage:

const storage = multer.diskStorage({
  destination: function(req, file, cb) {
    cb(null, 'uploads/');
  },
  filename: function(req, file, cb) {
    // Создание уникального имени файла
    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 });

Эта конфигурация дает вам контроль над:

  • Где хранятся файлы (destination)
  • Как они называются (filename)

Хранение в памяти

Если вам нужно обрабатывать файлы в памяти без сохранения на диск:

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

app.post('/upload', upload.single('file'), (req, res) => {
  // req.file.buffer содержит данные файла
  // Обработка файла в памяти (например, загрузка в облачное хранилище)
  
  // Пример: получение буфера файла и типа
  const fileBuffer = req.file.buffer;
  const fileType = req.file.mimetype;
  
  res.send('File processed');
});

Хранение в памяти полезно, когда:

  • Вы передаете файлы другому сервису (например, S3 или Cloudinary)
  • Вам нужно обработать файл перед сохранением
  • Вы работаете в бессерверных средах

Фильтрация файлов

Вы можете контролировать, какие файлы принимаются, используя опцию fileFilter:

const fileFilter = (req, file, cb) => {
  // Принимать только файлы изображений
  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
});

Ограничения размера и безопасность

Установка ограничений критически важна для безопасности и производительности:

const upload = multer({
  storage: multer.diskStorage({...}),
  limits: {
    fileSize: 5 * 1024 * 1024, // 5МБ в байтах
    files: 5 // Максимум 5 файлов на запрос
  }
});

Другие опции ограничений включают:

  • fieldNameSize: Максимальный размер имени поля (по умолчанию 100 байт)
  • fieldSize: Максимальный размер значения поля (по умолчанию 1МБ)
  • fields: Максимальное количество не-файловых полей (по умолчанию Infinity)
  • parts: Максимальное количество частей (полей + файлов) (по умолчанию Infinity)

Обработка ошибок

Ошибки Multer должны обрабатываться правильно для предоставления содержательной обратной связи:

app.post('/upload', (req, res) => {
  upload.single('file')(req, res, (err) => {
    if (err instanceof multer.MulterError) {
      // Произошла ошибка Multer
      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) {
      // Произошла неизвестная ошибка
      return res.status(500).json({ error: 'Server error' });
    }
    
    // Всё прошло хорошо
    res.json({ message: 'File uploaded successfully', file: req.file });
  });
});

Полный пример: Создание системы загрузки файлов

Давайте создадим полную систему загрузки файлов с фронтендом и бэкендом:

Бэкенд (server.js)

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

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

// Обслуживание статических файлов
app.use(express.static('public'));

// Настройка хранилища
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    const uploadDir = 'uploads';
    // Создание директории, если она не существует
    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);
  }
});

// Настройка фильтра файлов
const fileFilter = (req, file, cb) => {
  // Принимать изображения и 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);
  }
};

// Настройка multer
const upload = multer({
  storage: storage,
  fileFilter: fileFilter,
  limits: {
    fileSize: 10 * 1024 * 1024, // 10МБ
    files: 5
  }
});

// Маршрут для загрузки одного файла
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
      }
    });
  });
});

// Маршрут для загрузки нескольких файлов
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
    });
  });
});

// Получение списка загруженных файлов
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}`);
});

Фронтенд (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>

Интеграция с облачным хранилищем

Для продакшн-приложений часто требуется хранить файлы в облачном хранилище, а не на вашем сервере. Вот как интегрировать Multer с AWS S3:

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

// Настройка 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();

// Настройка multer для использования 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 // 5МБ
  }
});

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

Примечание: Вам потребуется установить пакет multer-s3: npm install multer-s3 aws-sdk.

Лучшие практики безопасности

При реализации загрузки файлов следуйте этим лучшим практикам безопасности:

  1. Проверяйте типы файлов: Всегда проверяйте MIME-типы и расширения
  2. Ограничивайте размеры файлов: Предотвращайте DoS-атаки от загрузки больших файлов
  3. Сканируйте на вредоносное ПО: Рассмотрите возможность интеграции сканирования вирусов для загруженных файлов
  4. Используйте безопасное хранилище: Не храните файлы в публичных директориях
  5. Генерируйте случайные имена файлов: Не используйте имена файлов, предоставленные пользователями, напрямую
  6. Реализуйте аутентификацию: Убедитесь, что только авторизованные пользователи могут загружать файлы
  7. Устанавливайте ограничения загрузки: Ограничивайте количество файлов на запрос и на пользователя

Заключение

Multer предоставляет надежное решение для обработки загрузки файлов в приложениях Node.js. Следуя шаблонам и практикам, изложенным в этой статье, вы можете реализовать безопасную, эффективную и удобную для пользователя функциональность загрузки файлов. Независимо от того, создаете ли вы простой загрузчик изображений или сложную систему управления документами, гибкий API Multer и возможности интеграции делают его отличным выбором для обработки multipart/form-data в ваших приложениях Express.

Часто задаваемые вопросы

Для больших файлов рассмотрите: увеличение ограничения размера в конфигурации Multer, реализацию загрузки по частям для очень больших файлов, использование потоковой передачи для обработки файлов без полной загрузки их в память, и реализацию отслеживания прогресса на фронтенде.

Да, Multer хорошо работает с TypeScript. Установите определения типов с помощью: 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