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