Como implementar upload de arquivos em múltiplas partes com multipart
1. Fundamentos do Multipart Upload
O formato multipart/form-data é o padrão da web para enviar arquivos binários combinados com dados textuais em uma única requisição HTTP. Diferente do application/x-www-form-urlencoded, que codifica tudo como pares chave-valor em formato URL, o multipart divide a requisição em partes separadas por um delimitador chamado boundary.
Cada parte possui seu próprio cabeçalho Content-Disposition (especificando o nome do campo e, opcionalmente, o nome do arquivo) e Content-Type. A estrutura típica é:
POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="nome"
João
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="arquivo"; filename="foto.jpg"
Content-Type: image/jpeg
[binary data]
------WebKitFormBoundary7MA4YWxkTrZu0gW--
Use upload multipart quando precisar enviar arquivos com metadados associados (ex: formulário com foto e descrição). Para uploads de streaming puro (sem campos extras), considere application/octet-stream.
2. Configuração do Ambiente e Dependências
Vamos usar Node.js com Express e Multer, a biblioteca mais popular para parsing multipart. Instale as dependências:
npm init -y
npm install express multer uuid
Para Python com Flask, a alternativa seria:
pip install flask werkzeug
Em Java Spring Boot, adicione commons-fileupload ao pom.xml.
Configure limites no Multer para evitar abusos:
const multer = require('multer');
const upload = multer({
limits: {
fileSize: 10 * 1024 * 1024, // 10MB por arquivo
files: 5, // máximo 5 arquivos por requisição
parts: 10 // máximo 10 partes (arquivos + campos)
},
fileFilter: (req, file, cb) => {
const tiposPermitidos = ['image/jpeg', 'image/png', 'application/pdf'];
if (!tiposPermitidos.includes(file.mimetype)) {
return cb(new Error('Tipo de arquivo não suportado'), false);
}
cb(null, true);
}
});
3. Implementação do Backend para Receber o Upload
Crie a rota POST com middleware Multer. O parser extrai automaticamente campos de texto e arquivos binários:
const express = require('express');
const multer = require('multer');
const path = require('path');
const { v4: uuidv4 } = require('uuid');
const app = express();
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/');
},
filename: (req, file, cb) => {
const ext = path.extname(file.originalname);
cb(null, `${uuidv4()}${ext}`);
}
});
const upload = multer({ storage, limits: { fileSize: 5 * 1024 * 1024 } });
app.post('/upload', upload.single('arquivo'), (req, res) => {
if (!req.file) {
return res.status(400).json({ erro: 'Nenhum arquivo enviado' });
}
res.status(200).json({
mensagem: 'Upload realizado com sucesso',
arquivo: {
nomeOriginal: req.file.originalname,
nomeSalvo: req.file.filename,
tamanho: req.file.size,
tipo: req.file.mimetype
}
});
});
app.listen(3000);
O objeto req.file contém metadados essenciais: fieldname, originalname, encoding, mimetype, destination, filename, path e size.
4. Estratégias de Armazenamento de Arquivos
Salvamento em disco local com organização por data:
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const dir = `uploads/${new Date().toISOString().slice(0, 10)}`;
fs.mkdirSync(dir, { recursive: true });
cb(null, dir);
},
filename: (req, file, cb) => {
cb(null, `${uuidv4()}${path.extname(file.originalname)}`);
}
});
Upload direto para cloud storage (Amazon S3 com multer-s3):
const multerS3 = require('multer-s3');
const s3 = new AWS.S3();
const uploadS3 = multer({
storage: multerS3({
s3: s3,
bucket: 'meu-bucket',
key: (req, file, cb) => {
cb(null, `${uuidv4()}-${file.originalname}`);
}
})
});
Uso de buffer em memória (apenas para arquivos pequenos):
const upload = multer({ storage: multer.memoryStorage() });
Para arquivos grandes (>100MB), sempre use streams para evitar estouro de RAM.
5. Tratamento de Erros e Validações Avançadas
Validação de tipo MIME combinada com verificação de assinatura (magic bytes):
const fileType = require('file-type');
async function validarArquivo(buffer) {
const tipo = await fileType.fromBuffer(buffer);
if (!tipo || !['image/jpeg', 'image/png'].includes(tipo.mime)) {
throw new Error('Tipo de arquivo inválido');
}
}
Middleware de erro global no Express:
app.use((err, req, res, next) => {
if (err instanceof multer.MulterError) {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(413).json({ erro: 'Arquivo muito grande (máx. 10MB)' });
}
if (err.code === 'LIMIT_FILE_COUNT') {
return res.status(400).json({ erro: 'Muitos arquivos (máx. 5)' });
}
}
res.status(400).json({ erro: err.message });
});
6. Upload de Múltiplos Arquivos Simultaneamente
No HTML, use o atributo multiple:
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="text" name="descricao" placeholder="Descrição">
<input type="file" name="arquivos" multiple accept="image/*">
<button type="submit">Enviar</button>
</form>
No backend, use upload.array():
app.post('/upload', upload.array('arquivos', 5), (req, res) => {
const arquivos = req.files.map(file => ({
nome: file.originalname,
tamanho: file.size
}));
res.json({
descricao: req.body.descricao,
arquivos: arquivos
});
});
Para processamento paralelo com Promise.all:
app.post('/upload', upload.array('arquivos'), async (req, res) => {
const resultados = await Promise.all(req.files.map(async (file) => {
// processar cada arquivo (ex: redimensionar, extrair metadados)
return { nome: file.filename, processado: true };
}));
res.json(resultados);
});
7. Otimizações e Boas Práticas de Performance
Streaming direto para S3 sem buffer intermediário:
const uploadStream = multer({
storage: multerS3({
s3: s3,
bucket: 'bucket',
key: (req, file, cb) => cb(null, `${Date.now()}-${file.originalname}`)
})
});
Implementação de progresso com Socket.IO:
// Cliente
const socket = io();
socket.on('progresso', (dados) => {
atualizarBarraProgresso(dados.percentual);
});
Chunking para arquivos muito grandes: divida o arquivo em partes de 5MB e envie requisições paralelas, remontando no servidor.
8. Segurança e Prevenção de Vulnerabilidades
Sanitização de nomes de arquivo:
const sanitize = require('sanitize-filename');
const nomeSeguro = sanitize(file.originalname);
Verificação de autenticação:
function autenticar(req, res, next) {
const token = req.headers['authorization'];
if (!token || !validarToken(token)) {
return res.status(401).json({ erro: 'Não autorizado' });
}
next();
}
app.post('/upload', autenticar, upload.single('arquivo'), handler);
Proteção contra bombas zip: verifique o tamanho descomprimido antes de extrair:
const yauzl = require('yauzl');
yauzl.fromBuffer(buffer, { lazyEntries: true }, (err, zipfile) => {
zipfile.on('entry', (entry) => {
if (entry.uncompressedSize > 100 * 1024 * 1024) {
throw new Error('Arquivo zip muito grande após descompressão');
}
});
});
Proteção contra slow read: defina timeouts no Express:
app.use(express.raw({ limit: '10mb', type: 'multipart/form-data' }));
Referências
- Documentação oficial do Multer — Guia completo sobre o middleware de upload multipart para Node.js/Express, com exemplos de storage em disco e memória
- MDN Web Docs: Enviando formulários com multipart/form-data — Explicação detalhada sobre o formato multipart e como enviar arquivos via JavaScript
- Werkzeug (Flask) File Uploads — Documentação oficial do parser multipart usado pelo Flask, com validações e exemplos práticos
- Amazon S3 Multipart Upload API — Guia oficial para upload de arquivos grandes em partes usando o SDK AWS, com controle de concorrência
- OWASP File Upload Cheat Sheet — Checklist de segurança para upload de arquivos, incluindo validação de tipo, prevenção de path traversal e proteção contra bombas zip