Como desenvolvedor Node.js, há uma boa chance de que em algum ponto você importou o módulo fs e escreveu algum código que interagiu com o sistema de arquivos.

O que você talvez não saiba é que o módulo fs é um módulo multiplataforma baseado em padrões e cheio de recursos que expõe não uma, mas três APIs que atendem a estilos de programação síncronos e assíncronos.

Neste artigo, exploraremos completamente o mundo do processamento de arquivos Node.js em sistemas Windows e Linux, com foco na API baseada em promessa do módulo fs.

Uma observação antes de começar

Todos os exemplos neste artigo devem ser executados em um ambiente Linux, mas muitos funcionarão no Windows também. Procure notas ao longo do artigo destacando exemplos que não funcionam no Windows. Com relação ao macOS-na maioria dos casos, o módulo fs funciona da mesma maneira que funcionaria no Linux, mas existem alguns comportamentos específicos do macOS que não são abordados neste artigo. Consulte a documentação oficial do Node.js para obter as nuances do macOS.

O completo o código-fonte para todos os exemplos está disponível em meu GitHub em briandesousa/node-file-process .

Apresentando o módulo fs

O módulo fs é um módulo principal integrado em Node.js . Ele existe desde o início, desde as versões originais do Node.js v0.x.

Desde seus primeiros dias, o módulo fs foi alinhado com Padrões de sistema de arquivos POSIX . Isso significa que o código que você escreve é ​​um tanto portável em vários sistemas operacionais, embora especialmente entre diferentes sabores de Unix e Linux.

Embora o Windows não seja um sistema operacional compatível com POSIX, a maioria das funções do módulo fs ainda serão trabalhar. No entanto, existem funções que não são portáveis ​​simplesmente porque certos recursos do sistema de arquivos não existem ou são implementados de forma diferente no Windows.

Ao revisarmos as funções do módulo fs, lembre-se de que as funções a seguir retornarão erros ou terá resultados inesperados no Windows:

Funções para modificar as permissões e propriedade do arquivo: chmod () chown () Funções para trabalhar com links físicos e virtuais: link () symlink () readlink () lutimes () lchmod () lchown () Alguns metadados não estão definidos ou exibem valores inesperados ao usar stat () e lstat ()

Desde o Node v10, o módulo fs inclui três APIs diferentes: síncrona, retorno de chamada e promessa. Todas as três APIs expõem o mesmo conjunto de operações do sistema de arquivos.

Este artigo se concentrará na API mais recente baseada em promessa. No entanto, pode haver circunstâncias em que você deseja ou precisa usar as APIs síncronas ou de retorno de chamada. Por esse motivo, vamos comparar todas as três APIs.

Comparando as APIs do módulo FS

API síncrona

A API síncrona expõe um conjunto de funções que bloqueiam a execução para realizar operações do sistema de arquivos. Essas funções tendem a ser as mais simples de usar quando você está apenas começando.

Por outro lado, elas são bloqueadoras de thread, o que é muito contrário ao design de E/S sem bloqueio do Node.js. Ainda assim, há momentos em que você deve processar um arquivo de forma síncrona.

Aqui está um exemplo de uso da API síncrona para ler o conteúdo de um arquivo:

import * as fs from’fs’; dados const=fs.readFileSync (caminho); console.log (dados);

API de retorno de chamada

A API de retorno de chamada permite que você interaja com o sistema de arquivos de forma assíncrona. Cada uma das funções da API de retorno de chamada aceita uma função de retorno de chamada que é chamada quando a operação é concluída. Por exemplo, podemos chamar a função readFile com uma função de seta que recebe um erro se houver uma falha ou recebe os dados se o arquivo for lido com sucesso:

import * as fs de’fs’; fs.readFile (path, (err, data)=> {if (err) {console.error (err);} else {console.log (`arquivo lido completo, dados: $ {data}`);}}) ;

Essa é uma abordagem sem bloqueio que geralmente é mais adequada para aplicativos Node.js, mas vem com seus próprios desafios. O uso de retornos de chamada na programação assíncrona geralmente resulta em inferno de retorno de chamada . Se você não tiver cuidado com a forma como estrutura seu código, pode acabar com uma pilha complexa de funções de retorno de chamada aninhadas que podem ser difíceis de ler e manter.

API Promise

Se APIs síncronas devem ser evitadas quando possível, e APIs de retorno de chamada podem não ser ideais, isso nos deixa com a promessa API:

import * as fsPromises from’fs/promises’; função assíncrona usingPromiseAPI (caminho) {promessa const=fsPromises.readFile (caminho); console.log (‘fazer outra coisa’); retorno aguarda promessa; }

A primeira coisa que você pode notar é a diferença nesta instrução de importação em comparação com os exemplos anteriores: a API de promessa está disponível no subcaminho de promessas. Se você estiver importando todas as funções na API de promessa, a convenção é importá-las como fsPromises. As funções de API síncronas e de retorno de chamada geralmente são importadas como fs.

Se você quiser manter o código de exemplo compacto, as instruções de importação serão omitidas dos exemplos subsequentes. As convenções de nomenclatura de importação padrão serão usadas para diferenciar entre APIs: fs para acessar funções síncronas e de retorno de chamada e fsPromises para acessar funções de promessa.

A API de promessa permite que você aproveite as vantagens do açúcar sintático assíncrono/aguarde do JavaScript para escrever código assíncrono de forma síncrona. A função readFile () chamada na linha 4 acima retorna uma promessa. O código a seguir parece ser executado de forma síncrona. Finalmente, a promessa é retornada da função. O operador await é opcional, mas como o incluímos, a função aguardará a conclusão da operação do arquivo antes de retornar.

É hora de fazer um teste da API de promessa. Fique confortável. Existem algumas funções a serem abordadas, incluindo aquelas que criam, lêem e atualizam arquivos e metadados de arquivos.

Trabalhando com arquivos

Usando identificadores de arquivo

A API de promessa fornece duas abordagens diferentes para trabalhar com arquivos .

A primeira abordagem usa um conjunto de funções de nível superior que aceita caminhos de arquivo. Essas funções gerenciam o ciclo de vida dos identificadores de recurso de arquivo e diretório internamente. Você não precisa se preocupar em chamar uma função close () quando terminar com o arquivo ou diretório.

A segunda abordagem usa um conjunto de funções disponíveis em um objeto FileHandle. Um FileHandle atua como uma referência a um arquivo ou diretório no sistema de arquivos. Aqui está como você pode obter um objeto FileHandle:

async function openFile (path) {let fileHandle; tente {fileHandle=await fsPromises.open (path,’r’); console.log (`abriu $ {path}, o descritor de arquivo é $ {fileHandle.fd}`); dados const=fileHandle.read ()} catch (err) {console.error (err.message); } finalmente {fileHandle?.close (); }}

Na linha 4 acima, usamos fsPromises.open () para criar um FileHandle para um arquivo. Passamos o sinalizador r para indicar que o arquivo deve ser aberto no modo somente leitura. Qualquer operação que tente modificar o arquivo falhará. (Você também pode especificar outros sinalizadores .)

O conteúdo do arquivo são lidos usando a função read (), que está diretamente disponível no objeto de manipulação de arquivo. Na linha 10, precisamos fechar explicitamente o identificador de arquivo para evitar possíveis vazamentos de memória.

Todas as funções disponíveis na classe FileHandle também estão disponíveis como funções de nível superior. Continuaremos a explorar as funções de nível superior, mas é bom saber que essa abordagem também está disponível.

Lendo arquivos

Ler um arquivo parece uma tarefa tão simples. No entanto, existem várias opções diferentes que podem ser especificadas dependendo do que você precisa fazer com um arquivo:

//exemplo 1: leitura simples const data=await fsPromises.readFile (path);//exemplo 2: ler um arquivo que não existe (cria um novo arquivo) const noData=await fsPromises.readFile (path, {flag:’w’});//exemplo 3: ler um arquivo e retornar seu conteúdo como uma string codificada em base64 const base64data=await fsPromises.readFile (path, {encoding:’base64′});//exemplo 4: lê um arquivo, mas aborta a operação antes que ela seja concluída const controller=new AbortController (); const {sinal}=controlador; promessa const=fsPromises.readFile (caminho, {sinal: sinal}); console.log (`começou a ler o arquivo em $ {path}`); controlador.abort (); console.log (‘operação de leitura abortada antes que pudesse ser concluída’) aguarda promessa;

O exemplo 1 é o mais simples possível, se tudo o que você deseja fazer é obter o conteúdo de um arquivo.

No exemplo 2, não sabemos se o arquivo existe, então passamos o w sinalizador do sistema de arquivos para criá-lo primeiro, se necessário.

O exemplo 3 demonstra como você altera o formato dos dados retornados.

O exemplo 4 demonstra como interromper uma operação de leitura de arquivo e abortá-la. Isso pode ser útil ao ler arquivos que são grandes ou lentos para ler.

Copiando arquivos

A função copyFile pode fazer uma cópia de um arquivo e dar a você algum controle sobre o que acontece se o arquivo de destino já existe:

//exemplo 1: crie uma cópia, sobrescreva o arquivo de destino se ele já existir aguarde fsPromises.copyFile (‘source.txt’,’dest.txt’);//exemplo 2: cria uma cópia mas falha porque o arquivo de destino já existe aguarda fsPromises.copyFile (‘source.txt’,’dest.txt’, fs.constants.COPYFILE_EXCL);//Erro: EEXIST: arquivo já existe, copyfile’source.txt’->’dest.txt’

O exemplo 1 substituirá dest.txt se já existir. No exemplo 2, passamos o sinalizador COPYFILE_EXCL para substituir o comportamento padrão e falharemos se dest.txt já existir.

Gravando arquivos

Existem três maneiras de gravar em um arquivo:

Anexar a um arquivo Gravar em um arquivo Truncar um arquivo

Cada uma dessas funções ajuda a implementar diferentes casos de uso.

//exemplo 1: anexar a um arquivo existente//conteúdo de data.txt before: 12345 await fsPromises.appendFile (‘data.txt’,’67890′);//conteúdo de data.txt após: 1234567890//exemplo 2: anexar a um arquivo que ainda não existe e aguardar fsPromises.appendFile (‘data2.txt’,’123′);//Erro: ENOENT: nenhum arquivo ou diretório, abra’data2.txt’//exemplo 3: grave em um arquivo existente//conteúdo de data3.txt antes de: 12345 await fsPromises.writeFile (‘data3.txt’,’67890′);//conteúdo de data3.txt após: 67890//exemplo 4: gravar em um arquivo que ainda não existe (novo arquivo é criado) await fsPromises.writeFile (‘data4.txt’,’12345′);//exemplo 5: truncar dados em um arquivo existente//conteúdo de data5.txt antes de: 1234567890 await fsPromises.truncate (‘data5.txt’, 5);//conteúdo de data5.txt após: 12345

Os exemplos 1 e 2 demonstram como usar a função appendFile para anexar dados a arquivos novos ou existentes. Se um arquivo não existir, appendFile o criará primeiro.

Os exemplos 3 e 4 demonstram como usar a função writeFile para gravar em arquivos novos ou existentes. A função writeFile também criará um arquivo se ele não existir antes de gravá-lo. No entanto, se o arquivo já existe e contém dados, o conteúdo do arquivo é sobrescrito sem aviso.

O exemplo 5 demonstra como usar a função truncar para cortar o conteúdo de um arquivo. Os argumentos que são passados ​​para esta função podem ser confusos no início. Você pode esperar que uma função de truncar aceite o número de caracteres a serem retirados do final do arquivo, mas, na verdade, precisamos especificar o número de caracteres a reter. No caso acima, você pode ver que inserimos um valor de 5 para a função truncar, que removeu os últimos cinco caracteres da string 1234567890.

Assistindo arquivos

A API de promessa fornece uma função de observação única e de alto desempenho que pode observar alterações em um arquivo.

const abortController=new AbortController (); const {sinal}=abortController; setTimeout (()=> abortController.abort (), 3000); const watchEventAsyncIterator=fsPromises.watch (caminho, {sinal}); setTimeout (()=> {fs.writeFileSync (caminho,’novos dados’); console.log (`modificado $ {caminho}`);}, 1000); para await (evento const de watchEventAsyncIterator) {console.log (`’$ {event.eventType}’evento de observação foi gerado para $ {event.filename}`); }//saída do console://modificado./data/watchTest.txt//evento de observação’alterar’foi gerado para watchTest.txt//observação em./data/watchTest.txt abortado

A função de observação pode observar um arquivo para alterações indefinidamente. Cada vez que uma mudança é observada, um evento de observação é gerado. A função de observação retorna um iterável assíncrono, que é essencialmente uma maneira da função retornar uma série ilimitada de promessas. Na linha 12, aproveitamos o for await… of syntactic sugar to wait for e iterar cada evento assistido à medida que ele é recebido.

Há uma boa chance de você não querer assistir a um arquivo indefinidamente para mudanças. O relógio pode ser abortado usando um objeto de sinal especial que pode ser acionado conforme necessário. Nas linhas 1 a 2, criamos uma instância de AbortController, que nos dá acesso a uma instância de AbortSignal que é finalmente passada para a função de observação. Neste exemplo, chamamos a função abort () do objeto de sinal após um período fixo de tempo (especificado na linha 3), mas você pode abortar como e sempre que precisar.

A função de observação também pode ser usado para observar o conteúdo de um diretório. Ele aceita uma opção recursiva opcional que determina se todos os subdiretórios e arquivos são observados.

Metadados do arquivo

Até agora, nos concentramos na leitura e modificação do conteúdo de um arquivo, mas você também pode precisar ler e atualizar os metadados de um arquivo. Os metadados do arquivo incluem tamanho, tipo, permissões e outras propriedades do sistema de arquivos.

A função stat é usada para recuperar metadados do arquivo, ou”estatísticas”, como tamanho do arquivo, permissões e propriedade.

//obtém todos os metadados do arquivo const fileStats=await fsPromises.stat (‘file1.txt’); console.log (fileStats)//console output://Stats {//dev: 2080,//mode: 33188,//nlink: 1,//uid: 1000,//gid: 1000,//rdev: 0 ,//blksize: 4096,//ino: 46735,//size: 29,//blocks: 8,//atimeMs: 1630038059841.8247,//mtimeMs: 1630038059841.8247,//ctimeMs: 1630038059841.8247,//birthtimeMs: 1630038059801.8247//atime: 2021-08-27T04: 20: 59.842Z,//mtime: 2021-08-27T04: 20: 59.842Z,//ctime: 2021-08-27T04: 20: 59.842Z,//hora do nascimento: 2021-08-27T04: 20: 59.802Z//} console.log (`tamanho de file1.txt é $ {fileStats.size}`);

Este exemplo demonstra a lista completa de metadados que podem ser recuperados para um arquivo ou diretório.

Lembre-se de que alguns desses metadados dependem do sistema operacional. Por exemplo, as propriedades uid e gid representam os proprietários do usuário e do grupo-um conceito aplicável aos sistemas de arquivos Linux e macOS, mas não aos sistemas de arquivos Windows. Zeros são retornados para essas duas propriedades ao executar esta função no Windows.

Alguns metadados de arquivo podem ser manipulados. Por exemplo, a função utimes é usada para atualizar os carimbos de data/hora de acesso e modificação em um arquivo:

const newAccessTime=new Date (2020,0,1); const newModificationTime=new Date (2020,0,1); esperar fsPromises.utimes (‘test1.txt’, newAccessTime, newModificationTime);

A função realpath é útil para resolver caminhos relativos e links simbólicos para caminhos completos:

//converter um caminho relativo para um caminho completo const realPath=await fsPromises.realpath (‘./test1.txt’); console.log (realPath);//saída do console:/home/brian/test1.txt//resolve o caminho real de um link simbólico apontando para/home/brian/test1.txt const symLinkRealPath=await fsPromises.realpath (‘./symlink1’); console.log (symLinkRealPath);//saída do console:/home/brian/test1.txt

Permissões e propriedade do arquivo

Por favor, tenha em mente, conforme continuamos nesta seção, que as funções de permissão e propriedade do arquivo são aplicáveis ​​ao Unix, Linux e sistemas operacionais macOS. Essas funções geram resultados inesperados no Windows.

Se você não tiver certeza se seu aplicativo tem as permissões necessárias para acessar ou executar arquivos em o sistema de arquivos , você pode usar a função de acesso para testá-lo:

//exemplo 1: verifique se um arquivo pode ser acessado try {await fsPromises.access (‘test1.txt’); console.log (‘test1.txt pode ser acessado’); } catch (err) {//EACCES: permissão negada, acesso’test1.txt’}//exemplo 2: verificar se um arquivo pode ser executado (aplica-se a sistemas baseados em Unix/Linux) try {await fsPromises.access (‘test2.txt’, fs.constants.X_OK); } catch (err) {//EACCES: permissão negada, acesso’test2.txt’}

As permissões do arquivo podem ser modificadas usando a função chmod. Por exemplo, podemos remover o acesso de execução de um arquivo passando uma string de modo especial:

//remover todos os acessos de execução de um arquivo await fsPromises.chmod (‘test1.txt’,’00666′);

A string de modo 00666 é um número especial de cinco dígitos composto por várias máscaras de bits que descrevem atributos de arquivo, incluindo permissões. Os últimos três dígitos são equivalentes ao modo de permissão de três dígitos que você deve estar acostumado a passar para chmod no Linux. A documentação do módulo fs fornece uma lista de máscaras de bits que podem ser usadas para interpretar esta string de modo.

A propriedade do arquivo também pode ser modificada usando a função chown:

//definir a propriedade do usuário e do grupo em um arquivo const root_uid=0; const root_gid=0; esperar fsPromises.chown (‘test1.txt’, root_uid, root_gid);

Neste exemplo, atualizamos o arquivo para que seja propriedade do usuário root e do grupo root. O uid do usuário root e o gid do grupo root são sempre 0 no Linux.

Trabalhando com links

Dica: As funções de link são aplicáveis ​​aos sistemas operacionais Unix/Linux. Essas funções geram resultados inesperados no Windows.

O módulo fs fornece uma variedade de funções que você pode usar para trabalhar duro e links virtuais ou simbólicos . Muitas das funções de arquivo que já vimos têm versões equivalentes para trabalhar com links. Na maioria dos casos, eles operam de forma idêntica também.

Antes de começarmos a criar links, vamos relembrar rapidamente os dois tipos de links com os quais trabalharemos.

Difícil vs.. links virtuais

Links físicos e virtuais são tipos especiais de arquivos que apontam para outros arquivos no sistema de arquivos. Um link virtual torna-se inválido se o arquivo ao qual está vinculado for excluído.

Por outro lado, um link físico apontando para um arquivo ainda será válido e conterá o conteúdo do arquivo, mesmo se o arquivo original for excluído. Links físicos não apontam para um arquivo, mas sim para os dados subjacentes de um arquivo. Esses dados são chamados de inode nos sistemas de arquivos Unix/Linux.

Podemos criar facilmente links físicos e físicos com o módulo fs. Use a função de link simbólico para criar links simbólicos e a função de link para criar links físicos.

//criar um link simbólico const softLink=await fsPromises.symlink (‘file.txt’,’softLinkedFile.txt’);//cria um link físico const hardLink=await fsPromises.link (‘file.txt’,’hardLinkedFile.txt’);

E se você quiser determinar o arquivo subjacente para o qual um link aponta? É aqui que entra a função readlink.

>//leia um link simbólico console.log (await fsPromises.readlink (‘softLinkedFile.txt’));//output: file.txt//lê um link físico… e falha console.log (await fsPromises.readLink (‘hardLinkedFile.txt’));//output: EINVAL: argumento inválido, readlink’hardLinkedFile.txt’

A função readlink pode ler links suaves, mas não links físicos. Um link físico é indistinguível do arquivo original ao qual está vinculado. Na verdade, todos os arquivos são tecnicamente links físicos. A função readlink essencialmente o vê como apenas mais um arquivo regular e lança um erro EINVAL.

A função unlink pode remover links físicos e virtuais:

//deletar um link virtual await fsPromises.unlink (‘softLinkedFile.txt’);//deleta um link físico/arquivo aguarda fsPromises.unlink (‘hardLinkedFile.txt’);

A função de desvinculação na verdade serve como uma função de propósito geral que também pode ser usada para excluir arquivos regulares, uma vez que eles são essencialmente o mesmo que um link físico. Além das funções de vincular e desvincular, todas as outras funções de link devem ser usadas com links simbólicos.

Você pode modificar os metadados de um link simbólico da mesma forma que faria com um arquivo normal:

//visualizar metadados do link flexível const linkStats=await fsPromises.lstat (caminho);//atualiza o acesso e modifica os carimbos de data/hora em um link simbólico const newAccessTime=new Date (2020,0,1); const newModifyTime=new Date (2020,0,1); esperar fsPromises.lutimes (‘softLinkedFile.txt’, newAccessTime, newModifyTime);//remove todos os acessos de execução de um soft link await fsPromises.lchmod (‘softLinkedFile.txt’,’00666′);//define a propriedade do usuário e do grupo em um link simbólico const root_uid=0; const root_gid=0; esperar fsPromises.lchown (‘softLinkedFile.txt’, root_uid, root_gid);

Além de cada função ser prefixada com um l, essas funções operam de forma idêntica às suas funções de arquivo equivalentes.

Trabalhando com diretórios

Não podemos simplesmente parar no processamento do arquivo. Se você estiver trabalhando com arquivos, é inevitável que também precise trabalhar com diretórios. O módulo fs fornece uma variedade de funções para criar, modificar e excluir diretórios.

Muito parecido com a função open que vimos anteriormente, a função opendir retorna um identificador para um diretório na forma de um objeto Dir. O objeto Dir expõe várias funções que podem ser usadas para operar naquele diretório:

let dir; tente {dir=await fsPromises.opendir (‘sampleDir’); dirents=esperar dir.read (); } catch (errar) {console.log (errar); } finalmente {dir.close (); }

Certifique-se de chamar a função close para liberar o identificador no diretório quando terminar.

O módulo fs também inclui funções que ocultam a abertura e o fechamento dos identificadores de recurso do diretório para você. Por exemplo, você pode criar, renomear e excluir diretórios:

//exemplo 1: criar um diretório await fsPromises.mkdir (‘sampleDir’);//exemplo 2: criar vários diretórios aninhados para esperar fsPromises.mkdir (‘nested1/nested2/nested3’, {recursive: true});//exemplo 3: renomear um diretório await fsPromises.rename (‘sampleDir’,’sampleDirRenamed’);//exemplo 4: remover um diretório await fsPromises.rmdir (‘sampleDirRenamed’);//exemplo 5: remover uma árvore de diretório aguarda fsPromises.rm (‘nested1’, {recursive: true});//exemplo 6: remover uma árvore de diretório, ignorar erros se ela não existir await fsPromises.rm (‘nested1’, {recursive: true, force: true});

Os exemplos 2, 5 e 6 demonstram a opção recursiva, que é especialmente útil se você não souber se um caminho existirá antes de criá-lo ou excluí-lo.

Existem duas opções para ler o conteúdo de um diretório. Por padrão, a função readdir retorna uma lista dos nomes de todos os arquivos e pastas diretamente abaixo do diretório solicitado.

Você pode passar a opção withFileTypes para obter uma lista de objetos de entrada do diretório Dirent. Esses objetos contêm o nome e o tipo de cada objeto do sistema de arquivos no diretório solicitado. Por exemplo:

//exemplo 1: obter nomes de arquivos e diretórios const files=await fsPromises.readdir (‘anotherDir’); para (arquivo const em arquivos) {console.log (arquivo); }//exemplo 2: obter arquivos e diretórios como objetos de entrada de diretório’Dirent’const dirents=await fsPromises.readdir (‘anotherDir’, {withFileTypes: true}); for (entrada const em dirents) {if (entry.isFile ()) {console.log (`nome do arquivo: $ {entry.name}`); } else if (entry.isDirectory ()) {console.log (`nome do diretório: $ {entry.name}`); } else if (entry.isSymbolicLink ()) {console.log (`nome do link simbólico: $ {entry.name}`); }}

A função readdir não fornece uma opção recursiva para ler o conteúdo dos subdiretórios. Você terá que escrever sua própria função recursiva ou contar com um módulo de terceiros como recursive-readdir] () .

Close ()

É hora de fechar () o identificador de recursos para este artigo. Vimos como trabalhar com arquivos, links e diretórios usando o módulo Node.js fs. O processamento de arquivos está disponível em Node.js pronto para uso, com todos os recursos e pronto para uso.