Upload de arquivos com NodeJS

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!

Criadores de Conteúdo

Foto do William Lima
William Lima
Fundador da Micilini

Inventor nato, escreve conteudos de programação para o portal da micilini.

Torne-se um MIC 🤖

Mais de 100 mic's já estão conectados na plataforma.