Dia 4 — Sistema de arquivos

Informatica · Conteudo · publicado em 30/09/2026
Dia 4 de 16

Sistema de arquivos

Aula 1

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çãoQual usar
script de uma vez, no fim do arquivoreadFileSync, writeFileSync
servidor atendendo requisiçãofs.promises (aula 2 de hoje)
package.json, config, .env na inicializaçãosync, 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
Aula 2

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.

ContextoQual
script de uma vez, no fim do arquivoreadFileSync, writeFileSync
servidor atendendo requisiçãofs.promises
package.json, config, .env na inicializaçãosync, 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étodoAssinaturaO que faz
mkdirmkdir(caminho, { recursive })cria diretório; recursive: true cria a árvore toda e não reclama se já existir
readdirreaddir(caminho, opcoes)lista os nomes do conteúdo
statstat(caminho)tamanho, data, permissões, isFile(), isDirectory()
unlinkunlink(caminho)remover arquivo: apaga um arquivo
rmrm(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 })