Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Як обробляти завантаження файлів в Express.js за допомогою Multer?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Multer** - це middleware для Express, яке парсить запити `multipart/form-data` і кладе завантажені файли в `req.file` або `req.files`. ```js const upload = multer({ storage: multer.memoryStorage() }); app.post('/upload', upload.single('avatar'), (req, res) => { res.json({ size: req.file.size }); // req.file.buffer готовий для S3 або диску }); ``` **Ключове:** у продакшені завжди додавай `limits: { fileSize }` та `fileFilter`, інакше будь-який клієнт може надсилати необмежені дані.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Multer** - це middleware для Express, яке парсить запити `multipart/form-data` і перетворює сирі бінарні потоки на об'єкти `req.file` або `req.files`, з якими можуть працювати обробники маршрутів. ## Теорія ### TL;DR - Multer як поштовий клерк: відкриває multipart-запит, відокремлює поля форми від файлів, передає обидва в обробник маршруту - Express за замовчуванням парсить JSON та URL-encoded тіла, але файлові завантаження ігнорує повністю. Multer закриває цю прогалину - `upload.single('field')` для одного файлу, `upload.array('field', n)` для кількох з одного поля, `upload.fields([...])` для змішаних - `memoryStorage()` тримає файл у RAM як `Buffer`, `diskStorage()` записує на диск. Вибір залежить від того, що робитимеш з файлом далі - Завжди встановлюй `limits` та `fileFilter` у продакшені. Без них будь-хто може надіслати 10GB «зображення» на твій сервер ### Швидкий приклад ```js const express = require('express'); const multer = require('multer'); const app = express(); const upload = multer({ storage: multer.memoryStorage() }); app.post('/upload', upload.single('avatar'), (req, res) => { // req.file = { fieldname, originalname, mimetype, size, buffer } res.json({ message: 'Файл отримано', size: req.file.size }); }); app.listen(3000); // POST /upload з form-data полем "avatar" = photo.jpg // req.file.buffer готовий до обробки або відправки на S3 ``` `upload.single('avatar')` - це middleware. Він спрацьовує до твого обробника, парсить потік і заповнює `req.file`. Обробник просто читає результат. ### Чому Express один не справиться Express нативно парсить тіла `application/json` та `application/x-www-form-urlencoded`. Але файлові завантаження приходять як `multipart/form-data` - єдиний бінарний потік, розбитий boundary-маркерами з заголовка `Content-Type`. Express цей потік просто ігнорує. Multer підключається до ланцюжка middleware, читає сирий потік через Busboy, ділить його по boundary-маркерах, буферизує файлові чанки в об'єкти `Buffer` і декодує текстові поля. Після Multer твій маршрут має `req.body` для полів і `req.file` для файлу. ### Опції зберігання Два вбудованих варіанти: `multer.memoryStorage()` тримає файл у RAM як `req.file.buffer`. Підходить для обробки одразу після отримання: resize через `sharp`, відправка на S3, OCR. Не підходить для великих файлів або великого навантаження. 100 користувачів по 50MB кожен - це стрибок RAM на 5GB. `multer.diskStorage({ destination, filename })` записує прямо на диск. Підходить для файлів, які треба роздавати статично або обробити пізніше. Шлях і ім'я файлу контролюєш через callback-функції. ```js const path = require('path'); const { v4: uuidv4 } = require('uuid'); const storage = multer.diskStorage({ destination: (req, file, cb) => cb(null, 'public/uploads/'), filename: (req, file, cb) => { const ext = path.extname(file.originalname); cb(null, `${uuidv4()}${ext}`); // напр. 550e8400-e29b-41d4-a716-446655440001.jpg } }); ``` Якщо використовувати `file.originalname` напряму як ім'я файлу, виникнуть колізії. Двоє користувачів з файлом `avatar.jpg` перезапишуть один одного. UUID або `Date.now() + random` вирішують цю проблему. ### Коли що використовувати - Один файл з одного поля форми: `upload.single('fieldName')` - Кілька файлів з одного поля: `upload.array('photos', 5)` (другий аргумент - максимальна кількість) - Файли з різних полів: `upload.fields([{name: 'avatar', maxCount: 1}, {name: 'docs', maxCount: 3}])` - Без файлів, тільки поля форми: Multer не потрібен, використовуй `express.urlencoded()` - Обробка в пам'яті та відправка на S3: `memoryStorage()` - Роздача файлів з диску: `diskStorage()` ### Валідація та обмеження Без обмежень Multer буферизує все, що приходить. Об'єкт `limits` і функція `fileFilter` закривають два основних вектори атак: ```js const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 5 * 1024 * 1024, // макс 5MB на файл files: 5 // макс 5 файлів на запит }, fileFilter: (req, file, cb) => { if (file.mimetype.startsWith('image/')) { cb(null, true); } else { cb(new Error('Дозволені тільки зображення'), false); } } }); ``` `fileFilter` викликається до буферизації файлу. `cb(null, false)` відхиляє файл. `cb(new Error(...))` передає помилку далі по ланцюжку middleware. Важливий момент: `mimetype` береться з заголовка `Content-Type` клієнта. Зловмисник може надіслати `image/jpeg` для PHP-файлу. Для реального захисту перевіряй magic bytes у буфері. JPEG починається з `0xFF 0xD8`, PNG з `0x89 0x50 0x4E 0x47`. Читай `req.file.buffer.slice(0, 4)` і порівнюй. ### Обробка помилок Multer кидає два типи помилок: `multer.MulterError` при перевищенні вбудованих лімітів і звичайний `Error` з `fileFilter` або твого коду. Обробляй обидва в одному middleware: ```js app.use((err, req, res, next) => { if (err instanceof multer.MulterError) { if (err.code === 'LIMIT_FILE_SIZE') { return res.status(400).json({ error: 'Файл занадто великий (макс 5MB)' }); } return res.status(400).json({ error: err.message }); } if (err.message === 'Дозволені тільки зображення') { return res.status(400).json({ error: err.message }); } next(err); }); ``` Якщо використовуєш `diskStorage` і файл зберігся на диск, але запит пізніше завалився - прибери файл: `fs.unlink(req.file.path, () => {})`. ### Типові помилки **Забутий `enctype="multipart/form-data"` в HTML-формі** Без нього браузер відправляє `application/x-www-form-urlencoded`. Multer пропускає такий запит, `req.file` дорівнює `undefined`, і обробник падає на `req.file.originalname`. ```html <!-- ламається без жодних натяків на причину --> <form action="/upload" method="post"> <input type="file" name="avatar"> </form> <!-- правильно --> <form action="/upload" method="post" enctype="multipart/form-data"> <input type="file" name="avatar"> </form> ``` **Відсутність `limits` у продакшені** Надсилання 2GB файлу на endpoint з `memoryStorage()` і без ліміту `fileSize` буферизує все в RAM. Node падає. Додавай `limits: { fileSize: 10 * 1024 * 1024 }` як мінімум. **`cb(null, false)` у `fileFilter` без виключення** Коли `fileFilter` викликає `cb(null, false)`, файл відхиляється тихо. `req.file` дорівнює `undefined`. Обробник падає при спробі прочитати `req.file.size`. Я бачив, як команди витрачали годину на дебаг того, чому завантаження зникають без помилок. Кидай виключення замість цього: `cb(new Error('Невірний тип файлу'), false)`. **Оригінальне ім'я файлу на диску** `cb(null, file.originalname)` призводить до колізій. Десятеро користувачів з файлом `photo.jpg` перепишуть один і той самий файл. Використовуй `uuidv4()` або `Date.now() + '-' + Math.round(Math.random() * 1e9)`. **`memoryStorage` для файлів, які все одно йдуть на диск** Якщо обробник робить `fs.writeFile('uploads/' + file.originalname, req.file.buffer)`, файл спочатку буферизується в RAM і лише потім записується на диск. Використовуй `diskStorage` - Multer стримить його напряму і RAM не витрачається. ### Де зустрічається у реальних проєктах - API профілів: `memoryStorage()`, потім `s3.putObject({ Body: req.file.buffer })` - NestJS: `@UseInterceptors(FileInterceptor('file'))` обгортає Multer, поведінка та сама - Strapi CMS: використовує `diskStorage` для медіабібліотеки - Пайплайни обробки зображень: `memoryStorage()`, потім `sharp(req.file.buffer).resize(200, 200)` Formidable - альтернатива для чистого Node.js без залежності від Express, з швидшим парсингом потоків. Busboy - це те, що Multer використовує під капотом. Якщо потрібна максимальна продуктивність при стримінгу гігабайтного файлу на S3 без буферизації, дивись у бік `@aws-sdk/lib-storage` або Busboy напряму. ### Питання на співбесіді **Q:** У чому різниця між `upload.single()`, `upload.array()` і `upload.fields()`? **A:** `single('avatar')` очікує один файл у названому полі, кладе його в `req.file`. `array('photos', 5)` приймає кілька файлів з одного поля, кладе в масив `req.files`. `fields([{name:'avatar'}, {name:'docs'}])` обробляє файли з різних полів, `req.files` стає об'єктом з ключами за іменами полів. **Q:** Як обробляти великі файли без краша Node? **A:** `memoryStorage` буферизує весь файл у RAM. Для великих файлів при навантаженні використовуй `diskStorage` - Multer стримить прямо на диск. Для S3 будуй stream pipeline через `@aws-sdk/lib-storage` Upload замість буферизації цілого файлу. **Q:** Як перевірити реальний тип файлу, а не тільки MIME? **A:** Перевіряй magic bytes у буфері. JPEG починається з `0xFF 0xD8`, PNG з `0x89 0x50 0x4E 0x47`. Читай `req.file.buffer.slice(0, 4)` і порівнюй. Поле `mimetype` береться з заголовка клієнта і може бути підроблене. **Q:** Що станеться, якщо файл пройшов `fileFilter`, але запис на диск провалився? **A:** Multer міг частково записати файл. Перевір `req.file.path` в обробнику помилок і виклич `fs.unlink(req.file.path, () => {})` для прибирання. Конструкція `try/finally` в обробнику теж підходить. **Q:** Чому Multer подвійно буферизує файли з `memoryStorage` при відправці на S3? **A:** Multer збирає всі вхідні чанки в один `Buffer` через Busboy. Потім ти створюєш `Readable` stream з цього буфера для `s3.putObject`. Два екземпляри в пам'яті одночасно. Щоб уникнути цього, для великих S3-завантажень пропускай Multer і передавай `req` напряму через `@aws-sdk/lib-storage` Upload зі стримінгом. ## Приклади ### Базовий: завантаження одного файлу в пам'ять ```js const express = require('express'); const multer = require('multer'); const app = express(); const upload = multer({ storage: multer.memoryStorage() }); app.post('/api/avatar', upload.single('avatar'), (req, res) => { if (!req.file) { return res.status(400).json({ error: 'Файл не надіслано' }); } // req.file.buffer містить сирі байти // req.file.mimetype = 'image/jpeg' // req.file.size = 45678 res.json({ name: req.file.originalname, size: req.file.size, type: req.file.mimetype }); }); app.listen(3000); // curl -F "avatar=@photo.jpg" http://localhost:3000/api/avatar ``` `upload.single('avatar')` виконується як middleware до обробника. Якщо ім'я поля форми не збігається з `'avatar'`, `req.file` дорівнює `undefined`. Завжди перевіряй через `if (!req.file)`. ### Середній: завантаження аватара на диск з валідацією та обробкою помилок ```js const path = require('path'); const { v4: uuidv4 } = require('uuid'); const multer = require('multer'); const storage = multer.diskStorage({ destination: (req, file, cb) => cb(null, 'public/uploads/avatars/'), filename: (req, file, cb) => { const ext = path.extname(file.originalname); cb(null, `avatar-${uuidv4()}${ext}`); // напр. avatar-550e8400-e29b-41d4-a716-446655440001.jpg } }); const upload = multer({ storage, fileFilter: (req, file, cb) => { if (file.mimetype.startsWith('image/')) { cb(null, true); } else { cb(new Error('Дозволені тільки зображення'), false); } }, limits: { fileSize: 5 * 1024 * 1024 } // 5MB }); app.post('/api/users/avatar', upload.single('avatar'), (req, res) => { if (!req.file) return res.status(400).json({ error: 'Файл не надіслано' }); res.json({ avatarUrl: `/uploads/avatars/${req.file.filename}` }); }); app.use((err, req, res, next) => { if (err instanceof multer.MulterError && err.code === 'LIMIT_FILE_SIZE') { return res.status(400).json({ error: 'Максимальний розмір файлу 5MB' }); } if (err.message === 'Дозволені тільки зображення') { return res.status(400).json({ error: err.message }); } next(err); }); ``` UUID-імена файлів запобігають колізіям при однакових іменах від різних користувачів. Middleware обробки помилок наприкінці перехоплює і вбудовані помилки Multer, і кастомні з `fileFilter`. Без нього помилки `fileFilter` стають необробленими 500-ми. ### Просунутий: завантаження файлу на S3 через буфер ```js const multer = require('multer'); const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3'); const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 10 * 1024 * 1024 }, // 10MB fileFilter: (req, file, cb) => { const allowed = ['image/jpeg', 'image/png', 'image/webp']; if (allowed.includes(file.mimetype)) cb(null, true); else cb(new Error('Непідтримуваний формат'), false); } }); const s3 = new S3Client({ region: 'us-east-1' }); app.post('/api/photos', upload.single('photo'), async (req, res) => { if (!req.file) return res.status(400).json({ error: 'Файл не надіслано' }); const key = `photos/${Date.now()}-${req.file.originalname}`; await s3.send(new PutObjectCommand({ Bucket: process.env.S3_BUCKET, Key: key, Body: req.file.buffer, // buffer з memoryStorage ContentType: req.file.mimetype })); res.json({ url: `https://${process.env.S3_BUCKET}.s3.amazonaws.com/${key}` }); }); ``` `memoryStorage` тут правильний вибір: буфер потрібен для відправки на S3, файл ніколи не торкається диску. Для файлів більше 50MB розглянь `@aws-sdk/lib-storage` Upload зі стримінгом - він робить multipart S3-завантаження без буферизації всього файлу одразу.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.