12k
All articles

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

Загрузка файлов в Node.js через middleware Multer и Express: настройка дискового хранилища, memory storage, фильтрации файлов и интеграции с AWS S3.

OpenReplay Team
OpenReplay Team
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, реализацию загрузки по частям для очень больших файлов, использование потоковой передачи для обработки файлов без полной загрузки их в память, и реализацию отслеживания прогресса на фронтенде.

Могу ли я использовать Multer с TypeScript?

Да, 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

We use cookies to improve your experience. By using our site, you accept cookies.