Upload de Arquivos para um Servidor Node com Express

Usuário fazendo upload de arquivos.

A necessidade de aplicações suportarem upload de arquivos é bem comum. Seja para um gateway de pagamentos confirmar a identidade de um usuário, uma rede social permitir que usuários troquem sua foto de perfil, enviar arquivos anexo a um email, enfim, são inúmeras as situações de uso.

Se você está em dúvidas sobre como trabalhar com arquivos em Node, esse post pode te ajudar.

Antes de falarmos sobre o upload em específico, vamos conversar um pouco a respeito de como é feito a leitura de arquivos em Node.

Leitura de Arquivos em Node

Para trabalhar com arquivos, existe um módulo, nativo do Node, chamado file system (fs). Dentre as coisas que ele nos permite fazer, estão: ler, criar, atualizar, deletar e renomear arquivos em nossa máquina.

Vamos testar seu funcionamento colocando a mão na massa. Para isso, basta criar um arquivo com extensão js em qualquer lugar do seu computador. O meu vai se chamar FileReader.js.

Em seguida, importar o módulo através da função require do Node. Vamos guardar o seu carregamento em uma constante chamada fs.

const fs = require('fs');

Como eu disse, o require apenas carrega o módulo na constante. Nada será executado ainda. A partir de agora, toda vez que nos referirmos a fs, estaremos falando do módulo carregado nela.

Em seguida, vamos pedir para que o módulo leia um arquivo. A função que faz isso se chama readFile e recebe como parâmetros: o caminho até o arquivo, e uma função callback – para ser executada quando a leitura do arquivo estiver pronta.

const fs = require('fs');
fs.readFile('imagem.jpg', () => {
});

Eu joguei um arquivo dentro da mesma pasta onde está o meu FileReader.js para fazer um teste. Por acaso, ele é uma imagem. Mas, poderia ser qualquer outro tipo de arquivo também.

A função de callback passada como segundo parâmetro para readFile, recebe também dois parâmetros: o erro ou o resultado da leitura.

const fs = require('fs');
fs.readFile('imagem.jpg', (erro, buffer) => {
});

Se houver uma falha na leitura do arquivo, o callback será chamado com o erro sendo passado no primeiro parâmetro. Nesse caso, o segundo parâmetro será null ou undefined.

Pegamos esse erro com um if e o logamos no console. Em seguida, fazemos um retorno para quem invocou a função de callback. Isso faz a execução do código ser interrompida no ponto onde está o return, evitando que as linhas abaixo do if executem em caso de erro.

const fs = require('fs');

fs.readFile('imagem.jpg', (erro, buffer) => {
	if(erro) {
		console.log(erro);
		return;
	}
});

Se tudo correu bem, nosso callback receberá null ou undefined no primeiro parâmetro. Isso fará com que não entremos no if, pois null, undefined, zero 0 ou string em branco '', são avaliados como falso em um if. Nesse caso, o segundo parâmetro do callback, receberá o arquivo lido por readFile em um bolsão de memória (buffer).

Logamos então uma mensagem de sucesso no console, e salvamos uma cópia da imagem com outro nome.

A função dessa biblioteca que escreve um arquivo, se chama writeFile. Ela recebe três parâmetros, sendo eles: o nome, o binario do arquivo a ser escrito (buffer) e, novamente, uma função callback, para tratar do erro ou resultado.

const fs = require('fs');

fs.readFile('imagem.jpg', (erro, buffer) => {
	if(erro) {
		console.log(erro);
		return;
	}

	console.log('Arquivo lido');

	fs.writeFile('imagem2.jpg', buffer, erro => {
		if(erro) {
			console.log(erro);
			return;
		}

		console.log('Arquivo escrito com sucesso');
	});
});

Aqui só estamos interessados em um possível erro de leitura. Se correr tudo bem com a escrita, logamos: "Arquivo escrito com sucesso".

A forma como acabamos de implementar a leitura e escrita de arquivos em Node, recebe o nome de Buffer Mode. Ou seja, carregamos o arquivo primeiro totalmente em memória, para depois fazer alguma coisa com ele.

Por exemplo: o callback de readFile somente será executado quando a imagem recebida no primeiro parâmetro estiver totalmente carregada em memória.

Isso funciona, mas apresenta algumas desvantagens. A principal delas, é o uso desnecessário de memória RAM no servidor.

O Node foi construído para trabalhar usando pouca memória. Esse é um dos grandes benefícios de usá-lo, pois permite uma redução drástica em consumo de recursos computacionais, como a memória RAM. Permitindo alta escalabilidade com baixo custo, se comparado a servidores web tradicionais.

Devido a essa característica, a engine V8, sobre a qual o Node roda, disponibiliza somente 1GB de memória para consumo. Isso quer dizer que: se tentarmos ler um arquivo maior do que esse tamanho utilizando a implementação descrita acima, derrubaremos o servidor. Mesmo que a máquina física onde ele estiver rodando tenha 16GB de RAM.

O grande trunfo do Node para fazer uso de pouca memória, está em ser muito eficiente em operações de I/O (input output). Essa característica casa muito bem com streams. Vamos entender.

Streaming de dados

A palavra inglesa stream significa "fluxo", e em computação representa uma sequência de dados disponibilizados ao longo do tempo.

Esses dados vão chegando em pequenos pedacinhos (chunks) e já podem ir sendo processados, sem esperar que o arquivo chegue primeiro totalmente e seja carregado em memória, para depois iniciar o processamento do mesmo.

Mas para utilizar este método, precisamos de alguém que entenda essa, como a forma sobre a qual desejamos trabalhar com os dados. Nesse sentido, os Engenheiros de Software do Node estenderam a biblioteca fs, permitindo que ela também trabalhasse com streams. Vamos ver como isso funciona.

Para demonstrar a leitura e escrita de arquivos com stream, criarei um novo arquivo em meu computador, com o nome StreamFileReader.js.

Como a biblioteca fs foi estendida, é ela que importaremos também em nosso novo arquivo para fazer o trabalho. Em seguida, criamos um fluxo de leitura com stream, através de createReadStream.

const fs = require('fs');
let fuxoDeLeitura = fs.createReadStream('imagem.jpg');

É possível enviar o resultado de uma stream, diretamente para a outra. Através da função pipe.

Sabendo disso, podemos passar a escrever os pedacinhos da nossa imagem, assim que eles forem sendo lidos. Em nosso exemplo, escrevemos uma cópia da mesma imagem com outro nome.

A lib de streams lança o evento "finish" quando um streaming de dados é finalizado, que pode ser ouvido através do escutador on. Definimos, dentro da função callback passada como segundo parâmetro para esse escutador, as ações a serem executadas quando o evento ocorrer.

const fs = require('fs');

let fuxoDeLeitura = fs.createReadStream('imagem.jpg');
let escrita = fuxoDeLeitura.pipe(fs.createWriteStream('imagem-escrita-com-stream.jpg'))

escrita.on('finish', () => {
	console.log('Arquivo escrito com stream');
});

Veja que a função createReadStream retorna um fluxo de leitura, que guardamos em uma variável. A partir dessa variável, invocamos pipe com o objetivo de termos um fluxo de escrita. Sobre essa nova variável, escutamos o evento "finish", disparando uma função callback quando ele acontecer.

A declaração dessas variáveis é desnecessária, uma vez que, podemos simplesmente encadear chamadas de funções, onde a saída de uma será passada como entrada para a que vier na sequência, tornando o código mais sucinto e legível. Conforme você pode ver abaixo.

const fs = require('fs');

fs.createReadStream('imagem.jpg')
.pipe(fs.createWriteStream('imagem-escrita-com-stream.jpg'))
.on('finish', () => {
	console.log('Arquivo escrito com stream');
});

Implementando Rota para Upload

Agora que entendemos a leitura e escrita de arquivos em Node, vamos utilizar o projeto resultante do artigo onde ensinamos a criar um servidor Node para, de fato, implementar uma rota que recebe arquivos. No final do artigo citado acima, você pode baixar o projeto.

Com ele em mãos, dentro de app, criamos a pasta files para receber os arquivos.

Na pasta controllers, um novo arquivo de rotas chamado upload.js. Onde, no primeiro parâmetro do método post, definimos uma String para representar a rota e, no segundo, a função de callback a ser executada mediante a um post na rota definida, conforme mostrado abaixo.

module.exports = app => {

	app.post('/upload/imagem', (req, res) => {

	});
};

Como iremos manipular arquivos nesta rota, importaremos, no inicio de upload.js, a lib fs. Em seguida, reaproveitamos nossa solução de leitura e escrita via streams discutida acima, fazendo os ajustes necessários.

Para começar, não precisamos mais criar um fluxo de leitura, pois isso já será feito pelo Express, no momento em que o cliente fizer a requisição http.

Pegamos somente a requisição vinda do cliente, que encontra-se no parâmetro req da função callback e abrimos um pipe para iniciar a escrita do arquivo.

const fs = require('fs');

module.exports = app => {

	app.post('/upload/imagem', (req, res) => {

		req.pipe(fs.createWriteStream('imagem-escrita-com-stream.jpg'))
		.on('finish', () => {
			console.log('Arquivo escrito com stream');
		});
	});
};

É claro que se mantivermos o nome do arquivo "chumbado" no código, todo novo upload de imagem sobrescreverá a anterior com o mesmo nome. Além de que, qualquer arquivo enviado, será salvo com extensão jpg.

Será responsabilidade do cliente enviar o nome do arquivo e sua extensão no cabeçalho da requisição http. Utilizaremos o cabeçalho filename do protocolo http para isso.

Você entenderá melhor essa parte, daqui a pouco quando fizermos o envio de um arquivo, simulando um cliente.

No lado do servidor, nós teremos acesso aos cabeçalhos de uma requisição, através de req.headers. Dentre eles, estamos interessados no filename.

Guardamos então esse nome em uma variável, a qual, concatenaremos, em seguida, com a String do caminho que indica o local onde o arquivo deverá ser salvo no servidor.

Em relação ao caminho, ele deve ser absoluto, partindo da raiz do projeto.

Para finalizar, substituímos nosso antigo log de sucesso, por uma resposta ao cliente.

const fs = require('fs');

module.exports = app => {

	app.post('/upload/imagem', (req, res) => {

		let filename = req.headers.filename;
		req.pipe(fs.createWriteStream('src/app/files/' + filename))
		.on('finish', () => {
			res.status(201).send("Upload concluído do arquivo " + filename);
		});
	});
};

Envio de arquivos em uma request http

Com a rota para receber o upload de arquivos pronta, vamos agora se colocar no lugar de um cliente, que deseja enviar um arquivo ao nosso servidor.

Existem ferramentas que simulam um cliente http, uma delas é o Curl, que será utilizado aqui.

Meu servidor está rodando em localhost na porta 3000. Na área de trabalho do meu Desktop, tenho uma imagem denominada some-coffee.jpg.

Desejo simular um cliente http fazendo o envio dessa imagem ao servidor. Como utilizarei o Curl, abro uma nova janela do terminal e navego até a pasta Desktop, a fim de não ter que me preocupar com o caminho para chegar à essa imagem.

Em seguida, informo ao Curl que desejo fazer um post para a rota criada. Indico (através do parâmetro --data-binary) que o dado enviado no corpo da requisição será em formato binário. Por fim, informo o caminho do arquivo e os headers da requisição.

O primeiro header, indica que o arquivo deve ser enviado em forma de stream, e – como ao enviar o arquivo em binário o sistema "desencana" do nome, mandando somente o arquivo – preciso enviar um segundo header, com o nome do arquivo.

curl -X POST http://localhost:3000/upload/imagem --data-binary @some-coffee.jpg \
-H "Content-type: application/octet-stream" \
-H "filename: some-coffee.jpg"

O arroba @ antes do nome do arquivo é um parâmetro do Curl para indicar que, após ele, haverá um caminho de arquivo. Já a contrabarra no final \, serve apenas para que o terminal do Linux entenda que desejamos quebrar a instrução para uma próxima linha, sem executá-la ainda.

Artigos Relacionados

Ir para o topo