Dia 4 — Sistema de arquivos
fs: ler e escrever arquivos
fs: ler e escrever arquivos
fs é o módulo de sistema de arquivos. As duas operações que formam qualquer
trabalho com arquivo são ler arquivo e escrever arquivo, e as duas existem em
versão síncrona e assíncrona. A escolha entre as duas APIs de mesmo nome não é
de gosto, é de contexto.
| Situação | Qual usar |
|---|---|
| script de uma vez, no fim do arquivo | readFileSync, writeFileSync |
| servidor atendendo requisição | fs.promises (aula 2 de hoje) |
package.json, config, .env na inicialização | sync, no começo do programa |
writeFileSync sobrescreve — sempre
fs.writeFileSync(caminho, conteudo, 'utf8'); fs.appendFileSync(caminho, maisConteudo, 'utf8');
writeFileSync faz duas coisas ao mesmo tempo: criar arquivo quando ele não
existe, e sobrescrever arquivo quando existe — apaga tudo e escreve do
zero. É o erro clássico — quem esperava acrescentar e sobrescreveu sem nenhum
aviso. Para somar no fim, appendFileSync.
O terceiro parâmetro é o encoding. 'utf8' é o único que interessa para
arquivo de texto, e sem ele o Node grava bytes crus — o arquivo sai
ilegível num editor de texto.
readFileSync devolve texto ou Buffer
const texto = fs.readFileSync(caminho, 'utf8'); // string const bytes = fs.readFileSync(caminho); // Buffer
O Buffer é o tipo binário do Node, e é o que vem quando se omite o encoding.
console.log de um Buffer imprime números de byte, não texto. Para converter:
bytes.toString('utf8').
Arquivo que não existe: existsSync e ENOENT
existsSync(caminho) devolve true ou false e nunca lança — é a forma barata
de perguntar. readFileSync num caminho inexistente lança um erro com
code: 'ENOENT', e é essa exceção que diz o motivo exato da falha:
try { fs.readFileSync(caminho, 'utf8'); } catch (erro) { console.error(erro.code + ': ' + erro.message); // stderr: o terminal console.log(' erro:', erro.code); // stdout: a página }
O par das duas linhas é o padrão do material: o console.error mostra o erro para
quem está no terminal, e o console.log é o que sobrevive na página, porque a
página embute o stdout.
JSON.stringify e JSON.parse
Arquivo de configuração, cache e exportação de dados são JSON, e o ciclo é
sempre o mesmo: JSON.stringify vira texto, JSON.parse volta a objeto.
fs.writeFileSync(caminho, JSON.stringify(dados, null, 2), 'utf8'); const dados = JSON.parse(fs.readFileSync(caminho, 'utf8'));
O segundo 2 do stringify é a indentação, e existe para o arquivo ficar
legível. O que o stringify não faz: função vira nada (a propriedade
desaparece), undefined some, e Date vira texto ISO. Quem precisa da data de
volta tem que converter na mão: new Date(lido.quando).
Apagar: unlinkSync e rmSync
unlinkSync apaga arquivo. rmSync apaga arquivo ou pasta, e com
{ recursive: true, force: true } apaga a pasta com tudo dentro sem reclamar se
ela já não existir. Todo exemplo que escreve precisa apagar no fim — arquivo
sobrando é estado que a próxima execução não espera.
Exemplo
'use strict'; // Exemplo da aula 1 do dia 4: ler e escrever arquivo com `fs`. // // A distincao que a aula precisa deixar clara e sync contra async, e ela NAO e // "qual dos dois e melhor": e "qual dos dois cabe aqui". `readFileSync` trava o // processo ate o arquivo inteiro voltar do disco; `readFile` devolve promessa e // o processo segue. Em script de uma vez o sincrono e mais simples e mais // honesto. Em servidor o sincrono e proibido, porque um `readFileSync` trava // TODAS as requisicoes ate o disco responder. // // O exemplo escreve num diretorio temporario e apaga no fim: arquivo de exemplo // nao fica para tras. const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); // Caminho FIXO, e nao `mkdtemp`: a saida deste exemplo vai para a pagina, e um // sufixo aleatorio faria a pagina divergir do terminal na segunda execucao. A // pasta e apagada no fim, entao ela pode ter o mesmo nome toda vez. const AREA = path.join(os.tmpdir(), 't2dia04-a1'); const ARQUIVO = path.join(AREA, 'notas.txt'); fs.rmSync(AREA, { recursive: true, force: true }); fs.mkdirSync(AREA, { recursive: true }); console.log('--- onde o exemplo escreve ---'); console.log(AREA); // --- 1. `writeFileSync`: cria, e nao promete nada --- // // `writeFileSync(caminho, conteudo)` cria o arquivo se ele nao existe, e // SOBRESCREVE tudo se existe. O segundo parametro e o encoding: `'utf8'` e o // unico que interessa para texto, e sem ele o Node grava bytes crus. fs.writeFileSync(ARQUIVO, 'primeira linha\n', 'utf8'); console.log(''); console.log('--- apos o primeiro writeFileSync ---'); console.log(fs.readFileSync(ARQUIVO, 'utf8').trim(), '(o arquivo foi criado)'); // --- 2. `writeFileSync` de novo SOBRESCREVE, e nao acrescenta --- // // Este e o erro classico: quem esperava acrescentar e apagou a primeira linha // sem nenhum aviso. A alternativa e `appendFileSync`, que soma no fim. fs.writeFileSync(ARQUIVO, 'so a segunda\n', 'utf8'); console.log(''); console.log('--- apos o SEGUNDO writeFileSync no mesmo caminho ---'); console.log(fs.readFileSync(ARQUIVO, 'utf8').trim(), '(a primeira linha sumiu)'); console.log('para acrescentar em vez de trocar: appendFileSync'); fs.appendFileSync(ARQUIVO, 'terceira linha\n', 'utf8'); fs.appendFileSync(ARQUIVO, 'quarta linha\n', 'utf8'); console.log(''); console.log('--- apos dois appendFileSync ---'); console.log(fs.readFileSync(ARQUIVO, 'utf8').trimEnd().split('\n').map((l, i) => ' ' + (i + 1) + ') ' + l).join('\n')); // --- 3. `readFileSync` devolve TEXTO, e `existsSync` diz se ha arquivo --- // // O detalhe do encoding e o que separa texto de binario. Sem `'utf8'`, o que // volta e um `Buffer` de bytes, e `console.log` dele imprime numeros. const comoTexto = fs.readFileSync(ARQUIVO, 'utf8'); const comoBuffer = fs.readFileSync(ARQUIVO); console.log(''); console.log('--- texto contra Buffer ---'); console.log("readFileSync(arq, 'utf8') devolve:", typeof comoTexto, '| string de', comoTexto.length, 'caracteres'); console.log('readFileSync(arq) devolve: ', typeof comoBuffer, '| objeto de', comoBuffer.length, 'bytes'); console.log('Buffer.toString("utf8") devolve de volta:', JSON.stringify(comoBuffer.toString('utf8').slice(0, 14)) + '...'); console.log('os dois tem o mesmo tamanho porque "linha" sao 5 letras e 5 bytes em utf8:', comoTexto.length === comoBuffer.length); // `existsSync` e sync e devolve booleano — e nao lanca quando o arquivo nao // existe. O `readFileSync` sem arquivo lanca `ENOENT`, que e a forma de descobrir // o motivo exato da falha. console.log(''); console.log('--- arquivo que existe e arquivo que nao ---'); console.log('existsSync(ARQUIVO):', fs.existsSync(ARQUIVO)); console.log('existsSync(nao-existe):', fs.existsSync(path.join(AREA, 'nao-existe.txt'))); try { fs.readFileSync(path.join(AREA, 'nao-existe.txt'), 'utf8'); } catch (erro) { console.error(erro.code + ': ' + erro.message); // stderr: so o terminal console.log(' erro de leitura:', erro.code, '-', 'o caminho existe?', erro.path === path.join(AREA, 'nao-existe.txt')); } // --- 4. JSON: os dois lados, `JSON.parse` e `JSON.stringify` --- // // Arquivo de configuracao, cache e exportacao de dados sao JSON. O erro de // `JSON.parse` e o mais enganoso do Node: ele traz o pedaco do texto QUE DEU // ERRO e a posicao, o que salva a hora de depurar. console.log(''); console.log('--- JSON.stringify e JSON.parse ---'); const dados = { turma: 't2', aulas: 32, ativo: true }; const texto = JSON.stringify(dados, null, 2); fs.writeFileSync(path.join(AREA, 'config.json'), texto, 'utf8'); console.log('gravado em config.json, ' + texto.split('\n').length + ' linhas'); const lido = JSON.parse(fs.readFileSync(path.join(AREA, 'config.json'), 'utf8')); console.log('lido de volta:', lido.turma, '|', lido.aulas, 'aulas | ativo =', lido.ativo); console.log('o que o JSON.stringify NAO faz: nao grava funcao nem undefined.'); const soFuncao = JSON.stringify({ soma: (a, b) => a + b, quando: new Date(0) }); console.log('stringify de { soma: funcao } devolve:', soFuncao); console.log(' a funcao sumiu; a Date virou texto ISO e precisa ser convertida de volta'); // --- 5. limpar --- // // O exemplo nao deixa arquivo para tras. `unlinkSync` apaga arquivo, `rmSync` // apaga arquivo ou pasta. O `rmSync` com `{ recursive: true, force: true }` // apaga a pasta e o que estiver dentro, sem reclamar se ja nao existir. fs.rmSync(AREA, { recursive: true, force: true }); console.log(''); console.log('area de trabalho apagada com rmSync({ recursive: true, force: true })'); console.log('ainda existe?', fs.existsSync(AREA));
Saída real
--- onde o exemplo escreve ---
/root/.hermes/cache/scratch/t2dia04-a1
--- apos o primeiro writeFileSync ---
primeira linha (o arquivo foi criado)
--- apos o SEGUNDO writeFileSync no mesmo caminho ---
so a segunda (a primeira linha sumiu)
para acrescentar em vez de trocar: appendFileSync
--- apos dois appendFileSync ---
1) so a segunda
2) terceira linha
3) quarta linha
--- texto contra Buffer ---
readFileSync(arq, 'utf8') devolve: string | string de 41 caracteres
readFileSync(arq) devolve: object | objeto de 41 bytes
Buffer.toString("utf8") devolve de volta: "so a segunda\nt"...
os dois tem o mesmo tamanho porque "linha" sao 5 letras e 5 bytes em utf8: true
--- arquivo que existe e arquivo que nao ---
existsSync(ARQUIVO): true
existsSync(nao-existe): false
erro de leitura: ENOENT - o caminho existe? true
--- JSON.stringify e JSON.parse ---
gravado em config.json, 5 linhas
lido de volta: t2 | 32 aulas | ativo = true
o que o JSON.stringify NAO faz: nao grava funcao nem undefined.
stringify de { soma: funcao } devolve: {"quando":"1970-01-01T00:00:00.000Z"}
a funcao sumiu; a Date virou texto ISO e precisa ser convertida de volta
area de trabalho apagada com rmSync({ recursive: true, force: true })
ainda existe? false
Promises no fs e operações de diretório
fs.promises e operações de diretório
A versão assíncrona de fs tem as mesmas assinaturas da síncrona, mas devolve
promessa. A escolha entre as duas não é de gosto: é de contexto.
| Contexto | Qual |
|---|---|
| script de uma vez, no fim do arquivo | readFileSync, writeFileSync |
| servidor atendendo requisição | fs.promises |
package.json, config, .env na inicialização | sync, no começo do programa |
A razão é concreta: readFileSync trava o processo até o arquivo inteiro
voltar do disco. Num script ninguém nota. Num servidor, um único readFileSync
lento segura todas as requisições até o disco responder — é o blocking do
event loop, e ele não aparece em nenhum teste de unidade: aparece quando o disco
do servidor está ocupado.
const fs = require('node:fs/promises'); await fs.writeFile(caminho, texto, 'utf8'); const texto = await fs.readFile(caminho, 'utf8');
mkdir, readdir, stat, unlink e rm
| Método | Assinatura | O que faz |
|---|---|---|
mkdir | mkdir(caminho, { recursive }) | cria diretório; recursive: true cria a árvore toda e não reclama se já existir |
readdir | readdir(caminho, opcoes) | lista os nomes do conteúdo |
stat | stat(caminho) | tamanho, data, permissões, isFile(), isDirectory() |
unlink | unlink(caminho) | remover arquivo: apaga um arquivo |
rm | rm(caminho, opcoes) | apaga arquivo ou pasta |
Para listar arquivos de um diretório, readdir é o método: ele devolve o que
existe dentro do caminho, e mkdir é quem cria diretório antes disso — um
readdir em pasta que não existe lança ENOENT.
Misturar unlink e rm é o erro que trava script: unlink em pasta lança
EPERM, e rm sem recursive em pasta com conteúdo lança ERR_FS_EISDIR. O
{ recursive: true, force: true } do rm resolve os dois casos e ainda não
reclama se o caminho já não existir — por isso é o par usado em toda limpeza.
O nome antigo de rm era rmdir, e ele ainda existe — mas só para diretório
vazio, o que quase nunca é o caso de uma pasta de trabalho. Quem removia pasta
com conteúdo usava rmdir mais um laço apagando arquivo por arquivo; rm com
recursive substituiu os dois.
readdir devolve nomes, não caminhos completos — quem precisa do caminho
completo monta com path.join(diretorio, nome). Com { withFileTypes: true }
devolve objetos com .isFile() e .isDirectory(), que é o jeito de distinguir
pasta de arquivo sem abrir cada um. Com { recursive: true } lista a árvore
inteira, e cada caminho vem com o prefixo do diretório pedido.
Com { recursive: true } o mkdir resolve o outro problema do caminho
relativo: criar diretório a partir de um caminho que sobe ../ depende de
onde o comando foi executado. Com path.resolve(__dirname, '..', 'dados') o
caminho absoluto nasce da pasta do próprio arquivo, e o mesmo código cria a
mesma árvore em qualquer máquina. É a razão de __dirname existir.
access e existsSync
fs.existsSync (do módulo fs, não do fs.promises) é o caminho síncrono.
Em código assíncrono o equivalente é access com throwIfNoEntry: false, que
devolve promessa em vez de lançar:
const existe = await fs.access(caminho, { throwIfNoEntry: false }) .then(() => true, () => false);
stat também responde à pergunta, e ainda entrega tamanho e data. A diferença é
que stat faz uma syscall completa e access só checa permissão.
O erro que só aparece em execução
Os erros de fs têm code próprio, e o code é o que se compara: ENOENT
(não existe), EEXIST (já existe), EPERM (sem permissão), EISDIR (é
diretório). erro.message sozinho não serve para decidir nada — a mesma
mensagem aparece em caminhos diferentes.
try { await fs.unlink(pasta); } catch (erro) { console.error(erro.code + ': ' + erro.message); // stderr: o terminal console.log(' erro:', erro.code); // stdout: a página }
O par das duas linhas é o padrão do material: o console.error mostra o erro para
quem está olhando o terminal, e o console.log é o que sobrevive na página,
porque a página embute o stdout.
Exemplo
'use strict'; // Exemplo da aula 2 do dia 4: `fs.promises` e operacao de diretorio. // // A diferenca com a aula 1 nao e de sintaxe, e de CONTRATURA. `writeFileSync` // trava o processo: em script de uma vez isso nao atrapalha; em servidor, um // unico `readFileSync` lento segura TODAS as requisicoes ate o disco responder. // Por isso a API assincrona devolve promessa e o `await` solta o event loop entre // uma operacao e outra. // // `readdir` + `stat` + `mkdir` + `unlink` sao as quatro operacoes de diretorio que // o material usa. Todas com `await`. const fs = require('node:fs/promises'); const os = require('node:os'); const path = require('node:path'); // Top-level `await` nao existe em CommonJS: este `.js` esta num projeto que // declara `"type": "commonjs"`, entao tudo precisa ficar dentro de uma funcao // `async`. Em ESM (aula 2 do dia 3) as mesma linhas ficariam no topo do arquivo. async function main() { // Caminho fixo, para a saida ser identica em execucoes diferentes. const AREA = path.join(os.tmpdir(), 't2dia04-a2'); // O `recursive: true` cria a arvore inteira de uma vez, e NAO reclama se ja // existir. Sem ele, `mkdir` em pasta que existe lanca `EEXIST`. await fs.mkdir(path.join(AREA, '2026', 'maio'), { recursive: true }); await fs.mkdir(path.join(AREA, '2026', 'junho'), { recursive: true }); console.log('--- arvore criada ---'); console.log(AREA + '/2026/{maio, junho}'); // --- 1. `writeFile` e `readFile` com `await` --- // // A assinatura e identica a da versao sync; o que muda e que devolve promessa. const caminhoJunho = path.join(AREA, '2026', 'junho', 'aula01.md'); await fs.writeFile(caminhoJunho, '# Aula 1\nconteudo de junho\n', 'utf8'); const texto = await fs.readFile(caminhoJunho, 'utf8'); console.log(''); console.log('--- writeFile e readFile ---'); console.log('conteudo lido:', JSON.stringify(texto)); // --- 2. `appendFile` --- await fs.appendFile(caminhoJunho, 'segunda linha\n', 'utf8'); const linhas = (await fs.readFile(caminhoJunho, 'utf8')).trimEnd().split('\n'); console.log(''); console.log('--- appendFile ---'); console.log('o arquivo agora tem', linhas.length, 'linhas:', linhas.join(' / ')); // --- 3. `readdir`: o que tem dentro de um diretorio --- // // Devolve um array de NOMES, nao de caminhos completos. `withFileTypes: true` // devolve objetos com `.isDirectory()` e `.isFile()`, que e o jeito de // distinguir pasta de arquivo sem abrir cada um. console.log(''); console.log('--- readdir ---'); const nomes = await fs.readdir(AREA); console.log('readdir(AREA):', JSON.stringify(nomes)); const dentroDeJunho = await fs.readdir(path.join(AREA, '2026', 'junho')); console.log("readdir('2026/junho'):", JSON.stringify(dentroDeJunho)); const comTipo = await fs.readdir(path.join(AREA, '2026'), { withFileTypes: true }); console.log("readdir('2026', { withFileTypes: true }):"); for (const entrada of comTipo) { console.log(' ' + entrada.name.padEnd(8) + (entrada.isDirectory() ? 'pasta' : 'arquivo')); } // `recursive: true` lista a arvore inteira, e cada caminho vem com o prefixo do // diretorio pedido. E a forma de "listar tudo que existe embaixo daqui". const tudo = await fs.readdir(AREA, { recursive: true }); console.log(''); console.log('readdir(AREA, { recursive: true }): ' + tudo.length + ' caminhos'); for (const item of tudo) console.log(' ' + item); // --- 4. `stat`: o que o SO e do arquivo --- // // `stat` traz tamanho, data e permissao. `isFile()` e `isDirectory()` dizem o // que o objeto e. `lstat` e o mesmo sem seguir link simbolico. console.log(''); console.log('--- stat ---'); const info = await fs.stat(caminhoJunho); console.log('tamanho:', info.size, 'bytes'); console.log('e arquivo?', info.isFile(), '| e pasta?', info.isDirectory()); console.log('modo (permissao):', (info.mode & 0o777).toString(8)); // O `mtime` de um arquivo recem-escrito e a hora do relogio, e muda a cada // execucao. Para a saida desta pagina ser sempre a mesma, o exemplo fixa a data // com `utimes` ANTES do `stat` — e o `stat` continua lendo o mtime real do // arquivo, so que um mtime previsivel. const DATA_FIXA = new Date('2026-05-04T00:00:00Z'); await fs.utimes(caminhoJunho, DATA_FIXA, DATA_FIXA); const depoisDaData = await fs.stat(caminhoJunho); console.log('data de escrita:', depoisDaData.mtime.toISOString().slice(0, 19) + 'Z'); console.log('(utimes fixou a data antes do stat, para a saida nao mudar de execucao para execucao)'); const infoPasta = await fs.stat(path.join(AREA, '2026')); console.log('a pasta 2026 e pasta?', infoPasta.isDirectory(), '| o arquivo e pasta?', infoPasta.isFile()); // --- 5. `access` e `existsSync`: perguntar antes de agir --- // // `fs.existsSync` (do modulo `fs`, nao do `fs.promises`) e o caminho sync. // Em codigo assincrono, `access` com `fs.constants.F_OK` e o equivalente. const existe = await fs.access(caminhoJunho).then(() => true, () => false); const naoExiste = await fs.access(path.join(AREA, 'x'), { throwIfNoEntry: false }) .then(() => true, () => false); console.log(''); console.log('--- access ---'); console.log('o arquivo de junho existe?', existe); console.log('o caminho x existe?', naoExiste); // --- 6. `unlink` e `rm` --- // // `unlink` apaga UM arquivo. `rm` apaga arquivo OU pasta, e e ele que recebe // `{ recursive: true, force: true }`. Misturar os dois e o erro que trava script: // `unlink` em pasta lanca `EPERM` e `rm` sem `recursive` em pasta com conteudo // lanca `ERR_FS_EISDIR`. await fs.unlink(caminhoJunho); console.log(''); console.log('--- unlink ---'); console.log('o arquivo de junho foi apagado, ainda existe?', await fs.access(caminhoJunho, { throwIfNoEntry: false }).then(() => true, () => false)); await fs.rm(AREA, { recursive: true, force: true }); console.log('rm(AREA, { recursive: true, force: true }) apagou a arvore toda'); console.log('a area ainda existe?', await fs.access(AREA, { throwIfNoEntry: false }) .then(() => true, () => false)); console.log('force: true faz o rm nao reclamar se o caminho ja nao existir'); // --- 7. o erro do `EPERM`, tratado --- // // Um `unlink` em diretorio e o erro de filesystem mais comum em script de // limpeza, e ele so aparece em execucao. console.log(''); console.log('--- erro de proposito: apagar uma pasta com unlink ---'); const alvo = path.join(AREA, 'pasta-que-foi-removida'); await fs.mkdir(alvo, { recursive: true }); try { await fs.unlink(alvo); } catch (erro) { console.error(erro.code + ': ' + erro.message); // stderr: o terminal console.log(' erro esperado:', erro.code, '-', 'unlink so apaga arquivo'); console.log(' o certo seria rm(caminho, { recursive: true })'); } await fs.rm(alvo, { recursive: true, force: true }); } main().catch((erro) => { console.error('falhou:', erro.code || erro.name, '-', erro.message); process.exit(1); });
Saída real
--- arvore criada ---
/root/.hermes/cache/scratch/t2dia04-a2/2026/{maio, junho}
--- writeFile e readFile ---
conteudo lido: "# Aula 1\nconteudo de junho\n"
--- appendFile ---
o arquivo agora tem 3 linhas: # Aula 1 / conteudo de junho / segunda linha
--- readdir ---
readdir(AREA): ["2026"]
readdir('2026/junho'): ["aula01.md"]
readdir('2026', { withFileTypes: true }):
junho pasta
maio pasta
readdir(AREA, { recursive: true }): 4 caminhos
2026
2026/junho
2026/maio
2026/junho/aula01.md
--- stat ---
tamanho: 41 bytes
e arquivo? true | e pasta? false
modo (permissao): 644
data de escrita: 2026-05-04T00:00:00Z
(utimes fixou a data antes do stat, para a saida nao mudar de execucao para execucao)
a pasta 2026 e pasta? true | o arquivo e pasta? false
--- access ---
o arquivo de junho existe? true
o caminho x existe? false
--- unlink ---
o arquivo de junho foi apagado, ainda existe? false
rm(AREA, { recursive: true, force: true }) apagou a arvore toda
a area ainda existe? false
force: true faz o rm nao reclamar se o caminho ja nao existir
--- erro de proposito: apagar uma pasta com unlink ---
erro esperado: EISDIR - unlink so apaga arquivo
o certo seria rm(caminho, { recursive: true })