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');
});
В этом примере:
- Мы создаем экземпляр Multer с папкой назначения для загрузок
- Мы используем промежуточное ПО
upload.single('document')
для обработки одиночной загрузки файла - Информация о загруженном файле доступна в
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
.
Лучшие практики безопасности
При реализации загрузки файлов следуйте этим лучшим практикам безопасности:
- Проверяйте типы файлов: Всегда проверяйте MIME-типы и расширения
- Ограничивайте размеры файлов: Предотвращайте DoS-атаки от загрузки больших файлов
- Сканируйте на вредоносное ПО: Рассмотрите возможность интеграции сканирования вирусов для загруженных файлов
- Используйте безопасное хранилище: Не храните файлы в публичных директориях
- Генерируйте случайные имена файлов: Не используйте имена файлов, предоставленные пользователями, напрямую
- Реализуйте аутентификацию: Убедитесь, что только авторизованные пользователи могут загружать файлы
- Устанавливайте ограничения загрузки: Ограничивайте количество файлов на запрос и на пользователя
Заключение
Multer предоставляет надежное решение для обработки загрузки файлов в приложениях Node.js. Следуя шаблонам и практикам, изложенным в этой статье, вы можете реализовать безопасную, эффективную и удобную для пользователя функциональность загрузки файлов. Независимо от того, создаете ли вы простой загрузчик изображений или сложную систему управления документами, гибкий API Multer и возможности интеграции делают его отличным выбором для обработки multipart/form-data в ваших приложениях Express.
Часто задаваемые вопросы
Для больших файлов рассмотрите: увеличение ограничения размера в конфигурации Multer, реализацию загрузки по частям для очень больших файлов, использование потоковой передачи для обработки файлов без полной загрузки их в память, и реализацию отслеживания прогресса на фронтенде.
Да, Multer хорошо работает с TypeScript. Установите определения типов с помощью: npm install @types/multer