Upload de arquivos com NodeJS
Uma das coisas mais complicadas que todo desenvolvedor pode se deparar é com a implementação da lógica de upload de arquivos 😏
O que parece ser uma coisa simples, acaba se tornando algo bem complexo com inúmeros erros... até que chega uma hora em que você acerta sem perceber (consegue fazer o upload da sua imagem, música, vídeo e etc...).
Particularmente existem duas coisas que EU (William Lima) odeio fazer quando trabalho com desenvolvimento de sistemas:
- Configurar um sistema de upload de arquivos, e
- Configurar um sistema de pagamento transparênte.
Sabe por que? Porque de todas as vezes que criei tais funcionalidades, sempre tive dores de cabeça, ou seja, a implementação não costumava funcionar de primeira 😂
Os motivos eram inúmeros... que iam desde CORS, compatibilidade, má documentação de APIs, permissionamento de pastas e etc...
Mas hoje... isso vai ser diferente 🤩
Porque eu vou ensinar você, a como criar um sistema de upload de arquivos com NodeJS, de duas formas distintas, a primeira delas envolve o upload de forma manual, e a segunda nós iremos usar a biblioteca multer.
Vamos nessa? 😉
Criando nosso projeto de testes
Vamos começar criando uma nova pasta dedicada ao projeto, no meu caso, eu criei uma pasta chamada de UPLOAD-FILES dentro da minha área de trabalho (desktop):
Com o seu terminal (Prompt de Comando) aberto na pasta raiz que acabamos de criar, precisamos inicializar o nosso projeto por meio do NPM
, sendo assim, execute o seguinte comando abaixo:
npm init -y
A flag -y
, como você já deve saber, cria um novo projeto de forma enxuta, respondendo SIM para tudo 😅
Em seguida, precisamos da biblioteca do express, pois iremos criar um servidor que vai gerenciar o upload desses arquivos para a gente:
npm install express
Além disso, vamos criar uma pasta chamada files dentro da pasta raiz do seu projeto, ela será responsável por armazenar os arquivos que vierem durante o upload 🙂
Vamos criar o nosso index.js
, que vai conter inicialmente 3 rotas:
const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();
const PORT = 3000;
// Configura o middleware para lidar com dados de formulário
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
// Serve o arquivo index.html
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'index.html'));
});
// Rota para upload de um único arquivo
app.post('/uploadSingle', (req, res) => {
//Lógica de upload de um único arquivo
});
// Rota para upload de múltiplos arquivos
app.post('/uploadMultiple', (req, res) => {
//Lógica de upload de múltiplos arquivos
});
// Cria a pasta files se não existir
if (!fs.existsSync(path.join(__dirname, 'files'))) {
fs.mkdirSync(path.join(__dirname, 'files'));
}
// Inicia o servidor
app.listen(PORT, () => {
console.log(`Servidor rodando na porta ${PORT}`);
});
Por fim, não se esqueça de criar o seu arquivo index.html
, que por sua vez, vai trabalhar com dois formulários de upload:
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Upload de Arquivos</title>
</head>
<body>
<h1>Upload de Arquivos</h1>
<!-- Formulário para upload de um único arquivo -->
<h2>Upload Único</h2>
<form action="/uploadSingle" method="POST" enctype="multipart/form-data">
<input type="file" name="singleFile" required>
<button type="submit">Enviar Arquivo</button>
</form>
<!-- Formulário para upload de múltiplos arquivos -->
<h2>Upload Múltiplo</h2>
<form action="/uploadMultiple" method="POST" enctype="multipart/form-data">
<input type="file" name="multipleFiles" multiple required>
<button type="submit">Enviar Arquivos</button>
</form>
</body>
</html>
Feito isso, vamos subir o nosso primeiro servidor em NodeJS (node ./index.js
) 😉
Fazendo o upload de um arquivo de forma manual
Se você percebeu, dentro do index.js
que acabamos de criar, nós temos uma rota chamada de /uploadSingle
, que será responsável por capturar o arquivo que será enviada no formulário de Upload Único, existente dentro do nosso arquivo HTML
:
Dentro do index.js
, vamos atualizar o código da nossa rota /uploadSingle
da seguinte forma:
// Rota para upload de um único arquivo
app.post('/uploadSingle', (req, res) => {
let formData = [];
req.on('data', chunk => {
formData.push(chunk); // Armazena os dados binários recebidos
});
req.on('end', () => {
const formDataBuffer = Buffer.concat(formData); // Concatena os dados em um único buffer
const boundary = '--' + req.headers['content-type'].split('boundary=')[1];
const boundaryBuffer = Buffer.from(boundary);
// Encontrando as partes do arquivo no formData
const boundaryIndex = formDataBuffer.indexOf(boundaryBuffer);
const nextBoundaryIndex = formDataBuffer.indexOf(boundaryBuffer, boundaryIndex + boundaryBuffer.length);
const filePart = formDataBuffer.slice(boundaryIndex + boundaryBuffer.length, nextBoundaryIndex);
// Extração do cabeçalho do arquivo (nome e tipo)
const fileHeaderEndIndex = filePart.indexOf('\r\n\r\n');
const header = filePart.slice(0, fileHeaderEndIndex).toString('utf8');
const contentDisposition = header.match(/filename="(.+?)"/); // Captura o nome original do arquivo
if (!contentDisposition || !contentDisposition[1]) {
return res.status(400).send('Nenhum arquivo foi enviado.');
}
// Extrair o nome do arquivo e sua extensão
const originalFilename = contentDisposition[1];
const fileExt = path.extname(originalFilename); // Captura a extensão do arquivo
const filename = Date.now() + fileExt; // Definindo o nome do arquivo com a extensão original
const filePath = path.join(__dirname, 'files', filename);
// Extração do conteúdo binário do arquivo
const fileData = filePart.slice(fileHeaderEndIndex + 4); // Pula os cabeçalhos e pega o conteúdo binário
// Salvando o arquivo binário corretamente
fs.writeFile(filePath, fileData, (err) => {
if (err) return res.status(500).send('Erro ao salvar o arquivo');
res.send(`Upload do arquivo ${originalFilename} feito com sucesso`);
});
});
});
Feito isso, vamos às explicações 😉
let formData = [];
req.on('data', chunk => {
formData.push(chunk); // Armazena os dados binários recebidos em um array de buffers
});
Tudo começa quando o usuário insere seu arquivo no Upload Único, e clica no botão [Enviar Arquivo].
Em seguida, o evento data
é disparado à medida que o servidor recebe partes (chunk) dos dados enviados no formulário. Cada parte dos dados é armazenada em um array
(formData), de modo que possamos lidar com o conteúdo completo no futuro.
req.on('end', () => {
const formDataBuffer = Buffer.concat(formData); // Concatena todos os chunks em um único buffer de dados
});
Após a conclusão de recebimento dos dados, o evento end
é disparado, concatenando todos os pedaços que foram recebidos dentro da variável formDataBuffer
.
É importante ressaltar que até este ponto, o arquivo de upload está sendo salvo na memoria da nossa aplicação.
const boundary = '--' + req.headers['content-type'].split('boundary=')[1];
const boundaryBuffer = Buffer.from(boundary);
Os uploads multipart (que permitem o envio de arquivos) são delimitados por "boundaries" (limites) definidos no cabeçalho Content-Type
. Este código identifica o valor do boundary usado na requisição e o converte para um Buffer, para que possamos localizar as partes relevantes dos dados.
const boundaryIndex = formDataBuffer.indexOf(boundaryBuffer);
const nextBoundaryIndex = formDataBuffer.indexOf(boundaryBuffer, boundaryIndex + boundaryBuffer.length);
const filePart = formDataBuffer.slice(boundaryIndex + boundaryBuffer.length, nextBoundaryIndex);
Os comandos acima, são responsáveis por localizar o início e o fim da parte que contém o arquivo em sí, usando o boundary para definir os limites.
O filePart
contém o cabeçalho e os dados do arquivo dentro dessa parte do formulário.
const fileHeaderEndIndex = filePart.indexOf('\r\n\r\n');
const header = filePart.slice(0, fileHeaderEndIndex).toString('utf8');
const contentDisposition = header.match(/filename="(.+?)"/); // Extrai o nome original do arquivo
Em seguida, o cabeçalho do arquivo é extraído da parte inicial de filePart
, que contém informações como o nome do arquivo (filename) e o seu tipo.
No código acima, observe que nós usamos uma expressão regular para capturar o nome original do arquivo (filename).
const originalFilename = contentDisposition[1];
const fileExt = path.extname(originalFilename); // Obtém a extensão do arquivo original
const filename = Date.now() + fileExt; // Nome único com a extensão correta
Após isso, o nome original do arquivo é extraído (originalFilename), e junto com a função path.extname()
nós conseguimos obter a extensão do arquivo com base no nome original (por exemplo: .zip, .jpeg, .png, .webp e etc...).
const fileData = filePart.slice(fileHeaderEndIndex + 4); // Pula o cabeçalho e obtém os dados binários
Após o cabeçalho, os dados binários do arquivo começam. No comando acima, nós fazemos o "slice" para pular o cabeçalho e capturar apenas o conteúdo do arquivo.
fs.writeFile(filePath, fileData, (err) => {
if (err) return res.status(500).send('Erro ao salvar o arquivo');
res.send(`Upload do arquivo ${originalFilename} feito com sucesso`);
});
Por fim, o arquivo é salvo no disco usando o fs.writeFile()
, com um nome gerado dinamicamente (usando Date.now()) e a extensão correta.
Se o salvamento for bem-sucedido, uma mensagem de sucesso é enviada ao cliente, e o arquivo é salvo dentro da pasta files 😄
Fazendo o upload de múltiplos arquivos de forma manual
Já para fazer o upload de múltiplos arquivos, o processo costuma ser o mesmo, basta adicionar a lógica abaixo dentro do seu index.js
, mais espeficiamente na rota /uploadMultiple
:
// Rota para upload de múltiplos arquivos
app.post('/uploadMultiple', (req, res) => {
let formData = [];
req.on('data', chunk => {
formData.push(chunk); // Armazena os dados binários recebidos
});
req.on('end', () => {
const formDataBuffer = Buffer.concat(formData); // Concatena todos os chunks recebidos
const boundary = '--' + req.headers['content-type'].split('boundary=')[1];
const boundaryBuffer = Buffer.from(boundary);
const boundaryEndBuffer = Buffer.from(boundary + '--');
let fileStartIndex = 0;
let uploadedFiles = []; // Armazena os nomes dos arquivos enviados
while (true) {
const boundaryIndex = formDataBuffer.indexOf(boundaryBuffer, fileStartIndex);
const nextBoundaryIndex = formDataBuffer.indexOf(boundaryBuffer, boundaryIndex + boundaryBuffer.length);
// Se não houver mais boundaries, termine o loop
if (nextBoundaryIndex === -1 || boundaryIndex === -1) {
break;
}
const filePart = formDataBuffer.slice(boundaryIndex + boundaryBuffer.length, nextBoundaryIndex);
// Extração do cabeçalho do arquivo
const fileHeaderEndIndex = filePart.indexOf('\r\n\r\n');
const header = filePart.slice(0, fileHeaderEndIndex).toString('utf8');
const contentDisposition = header.match(/filename="(.+?)"/);
if (!contentDisposition || !contentDisposition[1]) {
fileStartIndex = nextBoundaryIndex;
continue; // Se não houver arquivo, vai para a próxima parte
}
const originalFilename = contentDisposition[1];
const fileExt = path.extname(originalFilename); // Captura a extensão original do arquivo
const filename = Date.now() + '_' + originalFilename; // Cria um nome único com a extensão original
const filePath = path.join(__dirname, 'files', filename);
// Extração do conteúdo binário do arquivo
const fileData = filePart.slice(fileHeaderEndIndex + 4, filePart.length - 2); // Pula o cabeçalho e captura o conteúdo do arquivo
// Salvando o arquivo
fs.writeFileSync(filePath, fileData);
uploadedFiles.push(filename); // Armazena o nome do arquivo salvo
// Avança para o próximo boundary
fileStartIndex = nextBoundaryIndex;
}
// Retorna os arquivos que foram salvos
if (uploadedFiles.length > 0) {
res.send(`Upload dos seguintes arquivos feito com sucesso: ${uploadedFiles.join(', ')}`);
} else {
res.status(400).send('Nenhum arquivo foi enviado.');
}
});
});
Ao selecionar todos os seus arquivos que precisam ser enviados no formulário Upload Múltiplo:
Basta clicar em [Enviar Arquivos] para finalizar o upload 😉
A lógica de upload de múltiplos arquivos segue o mesma lógica da rota /uploadSingle
, a diferença é que, em vez de processar um único arquivo, ela itera sobre cada parte do formulário multipart
, identificando cada arquivo individualmente.
Para isso, utilizamos um loop (while(true)
) que percorre os limites (boundary) no corpo da requisição, extraindo tanto os metadados quanto também o conteúdo de cada arquivo.
Além disso, criamos nomes únicos para os arquivos enquanto preservamos suas extensões originais, salvando todos eles dentro da pasta files 😌
Fazendo o upload de arquivos com Multer
Se você deu uma olhada no tópico anterior, deve ter achado beeeem complexo a lógica de upload de arquivos pela quantidade de comandos alí existentes, certo?
E realmente ela é 🤯
E foi pensando em abstrair parte dessa complexidade, que lançaram uma biblioteca chamada Multer, que nada mais é do que um middleware, que facilita o processo de upload de arquivos em aplicações Express ou com NodeJS.
Ela foi feita para lidar com formulários multipart/form-data
, que é o formato comumente usado para enviar arquivos via HTTP
.
Seu funcionamento é bem simples, ele intercepta a requisição antes que nosso arquivo seja processada pelo roteador, e então, extrai os arquivos e os anexa ao objeto de requisição (req), para que futuramente você acessá-los diretamente na rota e realizar operações como salvá-los em disco, ou manipulá-los da forma como desejar.
Para instalar o Multer
, abra o seu terminal (Prompt de Comando) na pasta raiz do seu projeto, e execute o seguinte código:
npm i multer
Após a instalação, basta atualizar o seu index.js
da seguinte forma:
const express = require('express');
const multer = require('multer');
const path = require('path');
const app = express();
// Configura o storage do Multer
const storage = multer.diskStorage({
destination: (req, file, cb) => {
// Define a pasta onde os arquivos serão salvos
cb(null, 'files/');
},
filename: (req, file, cb) => {
// Extrai a extensão original do arquivo
const ext = path.extname(file.originalname);
// Gera o novo nome do arquivo com base no timestamp
const filename = Date.now() + ext;
// Retorna o nome final
cb(null, filename);
}
});
// Passa o storage configurado para o Multer
const upload = multer({ storage: storage });
// Serve o arquivo index.html
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'index.html'));
});
app.post('/uploadSingle', upload.single('singleFile'), (req, res) => {
// O arquivo está disponível em req.file
console.log(req.file);
res.send('Upload feito com sucesso!');
});
app.post('/uploadMultiple', upload.array('multipleFiles', 10), (req, res) => {
// Os arquivos estão disponíveis em req.files
console.log(req.files);
res.send('Upload de múltiplos arquivos feito com sucesso!');
});
app.listen(3000, () => {
console.log('Servidor rodando na porta 3000');
});
Vamos às explicações 😉
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'files/');
},
filename: (req, file, cb) => {
const ext = path.extname(file.originalname);
const filename = Date.now() + ext;
cb(null, filename);
}
});
No comando acima, nós estamos configurando a forma como os arquivos serão armazenados.
destination
: define a pasta (files/) onde os arquivos enviados serão armazenados.
filename
: cria um novo nome de arquivo com base no timestamp
atual e na extensão original do arquivo, o que garante que o arquivo mantenha sua extensão.
const upload = multer({ storage: storage });
O código acima, cria um middleware chamado de upload
utilizando a configuração de armazenamento definida anteriormente, o que permite gerenciar os uploads de arquivos de forma personalizada.
Por fim, basta informar o middleware acima dentro do método POST
:
app.post('/uploadSingle', upload.single('singleFile'), (req, res) => {
....
});
....
app.post('/uploadMultiple', upload.array('multipleFiles', 10), (req, res) => {
....
});
upload.single('singleFile')
: chama o middleware que processa um único arquivo de upload do campo cujo nome é singleFile
.
upload.array('multipleFiles', 10)
: chama o middleware processa o upload de até 10 arquivos do campo cujo o nome é multipleFiles
.
Por fim, basta testar o upload de seus arquivos usando a biblioteca:
Viu como é mais simples utilizar o Multer
? 🥳
Arquivos da lição
Os arquivos que você viu durante o decorrer desta lição, podem ser encontrados neste link.
Conclusão
Nesta lição, você aprendeu a fazer o upload de arquivos de forma manual e utilizando a biblioteca Multer.
Até a próxima lição!