O JavaScript tem sido uma das linguagens de script mais populares, mas por um longo período, não foi uma grande escolha para o desenvolvimento de aplicativos backend do lado do servidor. Então veio o Node.js, usado para criar aplicativos leves, orientados a eventos, construídos usando a linguagem de programação JavaScript.

Node.js é um JavaScript de código aberto runtime que está disponível para download e instalação gratuita em qualquer um dos principais sistemas operacionais (Windows, Mac, Linux). Ele tem se tornado cada vez mais popular entre os criadores de aplicativos nos últimos anos, e tem fornecido novas oportunidades de emprego para desenvolvedores de JavaScript que buscam uma especialidade.

Neste artigo, vamos aprender sobre como gerenciar o sistema de arquivos usando o Node.js. É fácil usar as APIs do Node.js para interagir com o sistema de arquivos e realizar muitas operações complexas, e saber como manobrar através delas irá aumentar sua produtividade.

Vamos começar!

Pré-requisitos para entender o sistema de arquivos Node.js

O principal pré-requisito é a instalação do Node.js no seu sistema operacional. O Node.js não requer nenhum hardware complexo para executar, facilitando o download e a instalação do Node.js em seu computador.

Ajudaria se você também tivesse um conhecimento básico de JavaScript para trabalhar em módulos Node.js como sistemas de arquivos (também conhecidos como “FS” ou “fs”). Um entendimento de alto nível das funções JavaScript, funções de retorno de chamada (callback) e promessas o ajudará a compreender esse assunto ainda mais rápido.

Módulo de sistema de arquivos do Node.js

Trabalhar com arquivos e diretórios é uma das necessidades básicas de um aplicativo fullstack. Seus usuários podem querer enviar imagens, currículos ou outros arquivos para um servidor. Ao mesmo tempo, seu aplicativo pode precisar ler arquivos de configuração, mover arquivos, ou mesmo mudar suas permissões programaticamente.

O módulo de sistema de arquivos Node.js tem tudo isso. Ele fornece várias APIs para interagir com os sistemas de arquivos de forma transparente. A maioria das APIs são customizáveis com opções e flags. Você também pode usá-las para realizar operações de arquivo tanto síncronas quanto assíncronas.

Antes de abordarmos detalhadamente o módulo de sistema de arquivos, vamos dar uma olhada no que o módulo Node.js é.

Módulos do Node.js

Os módulos Node.js são um conjunto de funcionalidades disponíveis como APIs para um programa de consumo a ser utilizado. Por exemplo, você tem o módulo fs para interagir com o sistema de arquivos. Da mesma forma, um módulo http utiliza suas funções para criar um servidor e muitas outras operações. O Node.js oferece muitos módulos para abstrair muitas funcionalidades de baixo nível para você.

Você pode fazer seus próprios módulos também. Com Node.js versão 14 e seguintes, você pode criar e usar os módulos Node.js de duas maneiras: Módulos CommonJS (CJS) e ESM (MJS). Todos os exemplos que veremos neste artigo estão no estilo CJS.

Trabalhando com arquivos no Node.js

Trabalhar com arquivos envolve várias operações com arquivos e diretórios (pastas). Agora vamos aprender sobre cada uma dessas operações com exemplos de código-fonte. Abra seu editor de código-fonte favorito e teste conforme a explicação.

Primeiro, importe o módulo fs para o seu arquivo-fonte para começar a trabalhar com os métodos do sistema de arquivos. No estilo CJS, usamos o método require para importar um método a partir de um módulo. Então, para importar e usar os métodos do módulo fs, você faria isso:

const { writeFile } = require('fs/promises');

Além disso, note que estamos importando o método writeFile a partir do pacote fs/promises. Queremos usar os métodos com promessas porque são mais recentes e fáceis de usar com as palavras-chave async/await e exigem menos código. Outras alternativas são os métodos síncronos e as funções de callback que veremos mais adiante.

Como criar e escrever em um arquivo

Você pode criar e escrever em um arquivo de três maneiras:

  1. Usando o método writeFile
  2. Usando o método appendFile
  3. Usando o método openFile

Estes métodos aceitam um caminho de arquivo e os dados como conteúdo para escrever no arquivo. Se o arquivo existe, eles substituem o conteúdo do arquivo. Caso contrário, eles criam um novo arquivo com o conteúdo.

1. Usando o método writeFile

O trecho de código abaixo mostra o uso do método writeFile. Comece criando um arquivo chamado createFile.js usando o snippet abaixo:

const { writeFile } = require('fs/promises');
async function writeToFile(fileName, data) {
  try {
    await writeFile(fileName, data);
    console.log(`Wrote data to ${fileName}`);
  } catch (error) {
    console.error(`Got an error trying to write the file: ${error.message}`);
  }
}

Note que estamos usando a palavra-chave await para chamar o método, uma vez que ele retorna uma promessa JavaScript. Uma promessa de sucesso irá criar/escrever no arquivo. Temos um bloco de tentativas para lidar com erros caso a promessa seja rejeitada.

Agora podemos chamar a função writeToFile:

writeToFile('friends.txt', 'Bob');

Em seguida, abra um prompt de comando ou terminal e execute o programa acima usando o seguinte comando:

node createFile.js

Ele irá criar um novo arquivo chamado friends.txt com uma linha que simplesmente diz:

Bob

2. Usando o método appendFile

Como o nome indica, o principal uso deste método é anexar e editar um arquivo. Entretanto, você também pode usar o mesmo método para criar um arquivo.

Observe a função abaixo. Usamos o método appendFile com a flag w para escrever um arquivo. A flag padrão para anexar a um arquivo é a:

const { appendFile} = require('fs/promises');

async function appendToFile(fileName, data) {
  try {
    await appendFile(fileName, data, { flag: 'w' });
    console.log(`Appended data to ${fileName}`);
  } catch (error) {
    console.error(`Got an error trying to append the file: {error.message}`);
  }
}

Agora, você pode chamar a função acima desta forma:

appendToFile('activities.txt', 'Skiing');

Em seguida, você pode executar o código no ambiente Node.js usando o comando node, como vimos anteriormente. Isso criará um arquivo chamado activities.txt com o conteúdo Skiing nele.

3. Usando o método open

O último método que vamos aprender para criar e escrever em um arquivo é o método open. Você pode abrir um arquivo usando a flag w (para “write”):

const { open} = require('fs/promises');

async function openFile(fileName, data) {
  try {
    const file = await open(fileName, 'w');
    await file.write(data);
    console.log(`Opened file ${fileName}`);
  } catch (error) {
    console.error(`Got an error trying to open the file: {error.message}`);
  }
}

Agora chame a função openFile com:

openFile('tasks.txt', 'Do homework');

Quando você executar o script usando o comando node, você terá um arquivo chamado tasks.txt criado com o conteúdo inicial:

Do homework

Como ler um arquivo

Agora que sabemos como criar e escrever em um arquivo, vamos aprender a ler o conteúdo do arquivo. Vamos usar o método readFile do módulo de sistema de arquivos para fazer isso.

Crie um arquivo chamado readThisFile.js com o seguinte código:

// readThisFile.js
const { readFile } = require('fs/promises');
async function readThisFile(filePath) {
  try {
    const data = await readFile(filePath);
    console.log(data.toString());
  } catch (error) {
    console.error(`Got an error trying to read the file: {error.message}`);
 }
}

Agora vamos ler todos os três arquivos que criamos invocando a função readThisFile:

readThisFile('activities.txt');
readThisFile('friends.txt');
readThisFile('tasks.txt');

Finalmente, execute o script usando o seguinte comando de Node:

node readThisFile.js

Você deve ver a seguinte saída no console:

Skiing
Do homework
Bob

Um ponto a ser observado aqui: O método readFile lê o arquivo de forma assíncrona. Isso significa que a ordem que você lê o arquivo e a ordem que você obtém uma resposta para imprimir no console pode não ser a mesma. Você tem que usar a versão síncrona do método readFile para obtê-lo em ordem. Veremos isso aqui em pouco tempo.

Como renomear um arquivo

Para renomear um arquivo, use o método de renomeação do módulo fs. Vamos criar um arquivo chamado renamee-me.txt. Vamos renomear este arquivo programaticamente.

Crie um arquivo chamado renomeFile.js com o seguinte código:

const { rename } = require('fs/promises');

async function renameFile(from, to) {
  try {
    await rename(from, to);
    console.log(`Renamed ${from} to ${to}`);
  } catch (error) {
    console.error(`Got an error trying to rename the file: ${error.message}`);
  }
}

Como você deve ter notado, o método de renomeação leva dois argumentos. Um é o arquivo com o nome da fonte, e o outro é o nome do alvo.

Agora vamos chamar a função acima para renomear o arquivo:

const oldName = "rename-me.txt";
const newName = "renamed.txt";
renameFile(oldName, newName);

Como antes, execute o arquivo de script usando o comando de Node para renomear o arquivo:

node renameFile.js

Como mover um arquivo

Mover um arquivo de um diretório para outro é similar a renomear seu caminho. Então, podemos usar o próprio método rename para mover arquivos.

Vamos criar duas pastas, from e to. Então vamos criar um arquivo chamado move-me.txt dentro da pasta from e to.

A seguir, escreveremos o código para mover o arquivo move-me.txt. Crie um arquivo chamado moveFile.js com o seguinte snippet:

const { rename } = require('fs/promises');
const { join } = require('path');
async function moveFile(from, to) {
  try {
    await rename(from, to);
    console.log(`Moved ${from} to ${to}`);
  } catch (error) {
    console.error(`Got an error trying to move the file: ${error.message}`);
  }
}

Como você pode ver, estamos usando o método rename exatamente como antes. Mas por que precisamos importar o método join do módulo path (sim, o caminho é outro módulo crucial do Node.js)?

O método join é usado para unir vários segmentos de caminho especificados para formar um caminho. O usaremos para formar o caminho dos nomes dos arquivos de origem e destino:

const fromPath = join(__dirname, "from", "move-me.txt");
const destPath = join(__dirname, "to", "move-me.txt");
moveFile(fromPath, destPath);

E é isso aí! Se você executar o script moveFile.js, você verá o arquivo move-me.txt movido para a pasta move-me.txt.

Como copiar um arquivo

Usamos o método copyFile do módulo fs para copiar um arquivo da fonte para o destino.

Observe o snippet de código abaixo:

const { copyFile } = require('fs/promises');
const { join } = require('path');
async function copyAFile(from, to) {
  try {
    await copyFile(from, to);
    console.log(`Copied ${from} to ${to}`);
  } catch (err) {
    console.error(`Got an error trying to copy the file: ${err.message}`);
  }
}

Agora você pode chamar a função acima com:

copyAFile('friends.txt', 'friends-copy.txt');

Ele irá copiar o conteúdo do friends.txt para o arquivo friends-copy.txt.

Agora, isso é ótimo, mas como você copia múltiplos arquivos?

Você pode usar a API Promise.all para executar múltiplas promessas em paralelo:

async function copyAll(fromDir, toDir, filePaths) {
  return Promise.all(filePaths.map(filePath => {
   return copyAFile(join(fromDir, filePath), join(toDir, filePath));
  }));
}

Agora você pode fornecer todos os caminhos de arquivos para copiar de um diretório para outro:

copyFiles('from', 'to', ['copyA.txt', 'copyB.txt']);

Você também pode usar esta abordagem para realizar outras operações como mover, escrever e ler arquivos em paralelo.

Como excluir um arquivo

Usamos o método unlink para excluir um arquivo:

const { unlink } = require('fs/promises');
async function deleteFile(filePath) {
  try {
    await unlink(filePath);
    console.log(`Deleted ${filePath}`);
  } catch (error) {
    console.error(`Got an error trying to delete the file: ${error.message}`);
  }
}

Lembre-se, você precisará fornecer o caminho para o arquivo para excluir:

deleteFile('delete-me.txt');

Note que se o caminho for um link simbólico para outro arquivo, o método de destravamento cancelará o link simbólico, mas o arquivo original será intocado. Falaremos mais sobre links simbólicos mais tarde.

Como mudar as permissões e a propriedade do arquivo

Você pode, em algum momento, querer mudar programaticamente as permissões dos arquivos. Isso pode ser muito útil para tornar um arquivo somente leitura ou totalmente acessível.

Usaremos o método chmod para mudar a permissão de um arquivo:

const { chmod } = require('fs/promises');
async function changePermission(filePath, permission) {
  try {
    await chmod(filePath, permission);
    console.log(`Changed permission to ${permission} for ${filePath}`);
  } catch (error) {
    console.error(`Got an error trying to change permission: ${error.message}`);
  }
}

Podemos passar o caminho do arquivo e o bitmask de permissão para alterar as permissões.

Aqui está a chamada de função para alterar a permissão de um arquivo para somente leitura:

changePermission('permission.txt', 0o400);

Semelhante à permissão, você também pode mudar a propriedade de um arquivo programaticamente. Usamos o método chown para fazer isso:

const { chown } = require('fs/promises');

async function changeOwnership(filePath, userId, groupId) {
  try {
    await chown(filePath, userId, groupId);
    console.log(`Changed ownership to ${userId}:${groupId} for ${filePath}`);
  } catch (error) {
    console.error(`Got an error trying to change ownership: ${error.message}`);
  }
}

Então chamamos a função com o caminho do arquivo, ID do usuário e ID do grupo:

changeOwnership('ownership.txt', 1000, 1010);

Como criar um symlink

O link simbólico (também conhecido como symlink) é um conceito de sistema de arquivos para criar um link para um arquivo ou pasta. Criamos links simbólicos para criar shortcodes para um arquivo/pasta de destino no sistema de arquivos. O módulo Node.js filesystem fornece o método symlink para criar um link simbólico.

Para criar o link simbólico, precisamos passar o caminho do arquivo alvo, o caminho do arquivo real e digitar:

const { symlink } = require('fs/promises');
const { join } = require('path');
async function createSymlink(target, path, type) {
  try {
    await symlink(target, path, type);
    console.log(`Created symlink to ${target} at ${path}`);
  } catch (error) {
    console.error(`Got an error trying to create the symlink: ${error.message}`);
  }
}

Podemos chamar a função:

createSymlink('join(__dirname, from, symMe.txt)', 'symToFile', 'file');

Aqui criamos um link simbólico chamado symToFile.

Como observar as alterações de um arquivo

Você sabia que você pode observar as alterações acontecendo em um arquivo? É uma ótima maneira de monitorar alterações e eventos, especialmente quando você não está esperando por eles. Você pode capturá-las e auditá-las para revisão posterior.

O método watch é a melhor maneira de assistir às alterações de arquivo. Há um método alternativo chamado watchFile, mas não é tão performante quanto o método watch.

Até agora, usamos o método de módulo de sistema de arquivos com palavras-chave async/await. Veremos os usos da função callback com este exemplo.

O método watch aceita o caminho do arquivo e uma função de chamada de retorno como argumentos. Sempre que uma atividade ocorre no arquivo, a função callback é chamada.

Podemos aproveitar o parâmetro event para obter mais informações sobre as atividades:

const fs = require('fs');
function watchAFile(file) {
  fs.watch(file, (event, filename) => {
    console.log(`${filename} file Changed`);
  });
}

Chame a função passando um nome de arquivo para watch:

watchAFile('friends.txt');

Agora estamos capturando automaticamente qualquer atividade no arquivo friends.txt.

Trabalhando com diretórios (pastas) no Node.js

Vamos aprender como realizar operações em diretórios ou pastas. Muitas das operações como renomear, mover e copiar são similares ao que vimos para os arquivos. Entretanto, métodos e operações específicas são utilizáveis apenas em diretórios.

Como criar um diretório

Usamos o método mkdir para criar um diretório. Você precisa passar o nome do diretório como um argumento:

const { mkdir } = require('fs/promises');
async function createDirectory(path) {
  try {
    await mkdir(path);
    console.log(`Created directory ${path}`);
  } catch (error) {
    console.error(`Got an error trying to create the directory: ${error.message}`);
  }
}

Agora podemos invocar a função createDirectory com um caminho de diretório:

createDirectory('new-directory');

Isso irá criar um diretório chamado new-directory.

Como criar um diretório temporário

Os diretórios temporários não são diretórios regulares. Eles têm um significado especial para o sistema operacional. Você pode criar um diretório temporário usando o método mkdtemp().

Vamos criar uma pasta temporária no diretório temporário do seu sistema operacional. Obtemos as informações para a localização do diretório temporário através do método tmpdir() do módulo os:

const { mkdtemp } = require('fs/promises');
const { join } = require('path');
const { tmpdir } = require('os');
async function createTemporaryDirectory(fileName) {
  try {
    const tempDirectory = await mkdtemp(join(tmpdir(), fileName));
    console.log(`Created temporary directory ${tempDirectory}`);
  } catch (error) {
    console.error(`Got an error trying to create the temporary directory: ${error.message}`);
  }
}

Agora, vamos chamar a função com o nome do diretório para criá-la:

createTemporaryDirectory('node-temp-file-');

Note que Node.js adicionará seis caracteres aleatórios no final do nome da pasta temporária criada para mantê-la única.

Como excluir um diretório

Você precisa usar o método rmdir() para remover/eliminar um diretório:

const { rmdir } = require('fs/promises');
async function deleteDirectory(path) {
  try {
    await rmdir(path);
    console.log(`Deleted directory ${path}`);
  } catch (error) {
    console.error(`Got an error trying to delete the directory: ${error.message}`);
  }
}

Em seguida, chame a função acima, passando o caminho da pasta que você deseja remover:

deleteDirectory('new-directory-renamed');

APIs síncronas vs assíncronas

Até agora, vimos muitos exemplos de métodos de sistema de arquivos, e todos eles são com usos assíncronos. No entanto, você pode precisar lidar com algumas operações de forma síncrona.

Um exemplo de operação síncrona é a leitura de vários arquivos um após o outro. O módulo fs tem um método chamado readFileSync() para fazer isso:

const { readFileSync } = require('fs');
function readFileSynchronously(path) {
  try {
    const data = readFileSync(path);
    console.log(data.toString());
  } catch (error) {
    console.error(error);
  }
}

Note que o método readFileSync() não é necessário a partir do pacote “fs/promises”. Isso é porque o método não é assíncrono. Portanto, você pode chamar a função acima com:

readFileSynchronously('activities.txt');
readFileSynchronously('friends.txt');
readFileSynchronously('tasks.txt');

Neste caso, todos os arquivos acima serão lidos na ordem em que as funções foram chamadas.

O módulo de sistema de arquivos Node.js oferece um método síncrono para outras operações como a operação lida. Use com sabedoria e somente na base do que for necessário. Os métodos assíncronos são muito mais úteis para a execução paralela.

Lidando com erros

Como qualquer programador sabe, é preciso esperar erros e estar preparado para lidar com eles ao realizar operações em arquivos ou diretórios. E se o arquivo não for encontrado, ou se você não tiver permissão para escrever nele? Existem (e provavelmente haverá) muitos casos em que você pode encontrar um erro.

Sempre deve-se envolver as chamadas de método com um bloco try-catch. Dessa forma, se ocorrer um erro, o controle será transferido para o bloco catch, onde você pode revisar e lidar com o erro. Como você pode ter notado em todos os exemplos acima, utilizamos o bloco try-catch para lidar com os erros que encontramos.

Resumo

Vamos rever os pontos-chave que abordamos neste tutorial:

  • O módulo Sistema de arquivos Node.js (fs) tem muitos métodos para ajudar em muitas tarefas de baixo nível.
  • Você pode realizar várias operações de arquivo como criar, escrever, renomear, copiar, mover, excluir e muitas outras.
  • Você pode fazer várias operações de diretório como criar, diretório temporário, mover, e muitas outras.
  • Todos os métodos podem ser invocados de forma assíncrona usando promessas JavaScript ou funções de callback.
  • Você também pode invocar os métodos de forma síncrona, se necessário.
  • Sempre prefira os métodos assíncronos ao síncrono.
  • Manuseie os erros com um bloco de tentativa cada vez que você interage com os métodos.

Agora que trabalhamos um pouco com o sistema de arquivos do Node.js, você deve ter uma boa compreensão de seus detalhes. Se você quiser aprofundar ainda mais seu conhecimento, talvez seja interessante explorar os fluxos (streams) do Node.js como uma progressão natural no aprendizado dos módulos do Node.js. Os fluxos são formas eficientes de lidar com a troca de informações, incluindo chamadas de rede, leitura/escrita de arquivos e muito mais.

Você pode encontrar todo o código-fonte usado neste artigo neste Repositório GitHub.

Você está planejando usar o Node.js para o seu próximo projeto? Compartilhe conosco o motivo da sua escolha na seção de comentários abaixo.