Загрузка файлов
Файлы из формы приходят в особом формате multipart/form-data, который встроенные middleware Express не разбирают. Для этого есть пакет multer: он сохраняет файл на диск и кладёт сведения о нём в req.file. Добавим к статье блога обложку.
Установка
npm install multerНастройка
Создайте файл src/middleware/upload.js. В нём описывается, куда и под каким именем сохранять файлы:
// src/middleware/upload.js
import path from 'node:path';
import crypto from 'node:crypto';
import multer from 'multer';
const storage = multer.diskStorage({
destination: 'public/uploads',
filename(req, file, callback) {
const extension = path.extname(file.originalname).toLowerCase();
callback(null, `${crypto.randomUUID()}${extension}`);
},
});
export const upload = multer({
storage,
limits: { fileSize: 2 * 1024 * 1024 },
fileFilter(req, file, callback) {
const allowed = ['image/jpeg', 'image/png', 'image/webp'];
callback(null, allowed.includes(file.mimetype));
},
});Файлы попадут в public/uploads/ под случайными именами, размер ограничен 2 МБ, принимаются только картинки JPEG, PNG и WebP. Папку public/uploads создайте заранее.
Форма
В форме views/posts/new.ejs нужны атрибут enctype и поле типа file:
<form method="post" action="/posts" enctype="multipart/form-data">
<input name="title">
<textarea name="body"></textarea>
<input type="file" name="cover" accept="image/*">
<button>Опубликовать</button>
</form>Без enctype="multipart/form-data" браузер отправит только имя файла, а не его содержимое.
Маршрут
Middleware upload.single('cover') ставится перед обработчиком. Аргумент — значение name у поля файла:
// src/routes/posts.js
import { upload } from '../middleware/upload.js';
router.post('/', upload.single('cover'), postsController.store);Обработчик
Multer разбирает и текстовые поля формы, поэтому req.body работает как прежде. Сведения о файле лежат в req.file:
// src/controllers/postsController.js
export function store(req, res) {
const post = {
id: posts.length + 1,
title: req.body.title,
body: req.body.body,
cover: req.file ? `/uploads/${req.file.filename}` : null,
};
posts.push(post);
res.redirect(`/posts/${post.id}`);
}Если файл не выбран или не прошёл фильтр, req.file будет undefined. На странице статьи обложку выводят так: <img src="<%= post.cover %>" alt=""> внутри условия if (post.cover).
Поле req.file | Что содержит |
|---|---|
filename | Имя сохранённого файла |
originalname | Имя файла на компьютере посетителя |
mimetype | Тип файла, например image/png |
size | Размер в байтах |
Слишком большой файл
Если файл больше лимита, multer передаст в обработчик ошибок ошибку с кодом LIMIT_FILE_SIZE. Её можно показать посетителю понятным текстом:
// src/app.js
import multer from 'multer';
app.use((error, req, res, next) => {
if (error instanceof multer.MulterError) {
return res.status(400).send('Файл слишком большой: не больше 2 МБ');
}
next(error);
});Этот обработчик ставят перед общим обработчиком ошибок; остальные ошибки он передаёт дальше.
Частая ошибка
Не сохраняйте файл под именем, которое прислал посетитель. Имя ../../src/app.js или совпадающее с чужим файлом может перезаписать важные данные. Генерируйте имя на сервере, как в настройке выше, а оригинальное имя, если нужно, храните отдельно в данных статьи.