Модуль fs — файловая система

Зачем нужно

Модуль fs (file system) дает Node.js доступ к файловой системе: чтение, запись, удаление файлов, создание директорий, отслеживание изменений. Это основа для любого серверного приложения -- от чтения конфигов до обработки загруженных файлов и ведения логов.

Где используется

  • Чтение конфигурационных файлов (JSON, YAML, .env)
  • Запись логов, отчётов, экспорт данных
  • Работа с загруженными файлами (upload → сохранение)
  • CLI-инструменты (генерация файлов, scaffolding)
  • Статические файлы веб-сервера
  • Потоковая обработка больших файлов

Предпосылки

  • Что такое Node.js — модульная система
  • path — построение путей к файлам
  • Понимание callback-ов и async/await

Три стиля API

const fs = require('fs');              // Callback + Sync API
const fsPromises = require('fs/promises'); // Promise API (рекомендуется)

// 1. Callback (оригинальный)
fs.readFile('data.txt', 'utf-8', (err, data) => {
  if (err) throw err;
  console.log(data);
});

// 2. Синхронный (блокирующий)
const data = fs.readFileSync('data.txt', 'utf-8');
console.log(data);

// 3. Promise (рекомендуется)
const data2 = await fsPromises.readFile('data.txt', 'utf-8');
console.log(data2);

Правило: используй fs/promises + async/await везде. Синхронные методы (*Sync) допустимы только при инициализации (чтение конфига при старте).


Чтение файлов

readFile / readFileSync

const fs = require('fs/promises');

// Текстовый файл
async function readText() {
  const content = await fs.readFile('config.json', 'utf-8');
  const config = JSON.parse(content);
  console.log(config.port); // 3000
}

// Бинарный файл (без encoding — возвращает Buffer)
async function readBinary() {
  const buffer = await fs.readFile('image.png');
  console.log(buffer.length); // размер в байтах
  console.log(buffer[0], buffer[1]); // 137 80 (PNG magic bytes)
}

// Синхронное чтение (только при старте приложения)
const pkg = JSON.parse(
  require('fs').readFileSync('package.json', 'utf-8')
);

Проверка существования файла

const fs = require('fs/promises');

// ✅ Правильно: access
async function fileExists(path) {
  try {
    await fs.access(path);
    return true;
  } catch {
    return false;
  }
}

// ❌ Устарело: fs.exists (deprecated)
// ❌ Антипаттерн: проверить → прочитать (race condition)
//    Между проверкой и чтением файл может быть удалён

// ✅ Лучше: просто читать и ловить ошибку
async function safeRead(path) {
  try {
    return await fs.readFile(path, 'utf-8');
  } catch (err) {
    if (err.code === 'ENOENT') {
      console.log('Файл не найден');
      return null;
    }
    throw err; // другая ошибка — пробросить
  }
}

Запись файлов

writeFile

const fs = require('fs/promises');

// Запись текста (перезаписывает файл целиком)
await fs.writeFile('output.txt', 'Hello, Node.js!\n');

// Запись JSON
const data = { users: [{ name: 'Anna', age: 25 }] };
await fs.writeFile(
  'users.json',
  JSON.stringify(data, null, 2), // с отступами
  'utf-8'
);

// Запись Buffer
const buffer = Buffer.from([0x48, 0x65, 0x6c, 0x6c, 0x6f]);
await fs.writeFile('binary.dat', buffer);

appendFile

const fs = require('fs/promises');

// Дописать в конец файла (не перезаписывать)
await fs.appendFile('app.log', `[${new Date.toISOString()}] Server started\n`);

// Простой логгер
async function log(message) {
  const timestamp = new Date.toISOString();
  await fs.appendFile('app.log', `[${timestamp}] ${message}\n`);
}

await log('User logged in');
await log('Request to /api/users');

Флаги записи

const fs = require('fs/promises');

// Все флаги:
// 'w'  — запись (создать / перезаписать)      ← writeFile default
// 'a'  — дозапись (создать / дописать)         ← appendFile default
// 'r'  — чтение                                ← readFile default
// 'r+' — чтение и запись
// 'wx' — запись, ошибка если файл существует

// Создать файл только если НЕ существует
await fs.writeFile('config.json', '{}', { flag: 'wx' });
// Если файл есть → Error: EEXIST

Работа с директориями

mkdir / readdir

const fs = require('fs/promises');

// Создать директорию
await fs.mkdir('logs');

// Создать вложенные директории (recursive)
await fs.mkdir('data/users/avatars', { recursive: true });
// Создаст data/, data/users/, data/users/avatars/

// Прочитать содержимое директории
const files = await fs.readdir('src');
console.log(files); // ['index.js', 'utils.js', 'lib']

// С информацией о типе (файл или директория)
const entries = await fs.readdir('src', { withFileTypes: true });
for (const entry of entries) {
  const type = entry.isDirectory ? 'DIR ' : 'FILE';
  console.log(`${type} ${entry.name}`);
}
// DIR  lib
// FILE index.js
// FILE utils.js

// Рекурсивное чтение (Node.js 18.17+)
const allFiles = await fs.readdir('src', { recursive: true });
console.log(allFiles);
// ['index.js', 'utils.js', 'lib', 'lib/helpers.js', 'lib/db.js']

Информация о файле — stat

const fs = require('fs/promises');

const stats = await fs.stat('package.json');

console.log(stats.isFile);         // true
console.log(stats.isDirectory);    // false
console.log(stats.size);             // 1234 (байт)
console.log(stats.birthtime);        // 2024-01-15T10:30:00.000Z (создание)
console.log(stats.mtime);            // 2024-03-20T14:00:00.000Z (изменение)

// lstat — не переходит по символическим ссылкам
const linkStats = await fs.lstat('symlink.txt');
console.log(linkStats.isSymbolicLink); // true

Удаление, переименование, копирование

const fs = require('fs/promises');

// Удалить файл
await fs.unlink('temp.txt');

// Удалить директорию (рекурсивно)
await fs.rm('build', { recursive: true, force: true });
// force: true — не ошибаться если не существует

// Переименовать / переместить файл
await fs.rename('old-name.txt', 'new-name.txt');
await fs.rename('file.txt', 'archive/file.txt'); // перемещение

// Копировать файл (Node.js 16+)
await fs.copyFile('source.txt', 'backup.txt');

// Копировать директорию (Node.js 16.7+)
await fs.cp('src', 'src-backup', { recursive: true });

watch — отслеживание изменений

const fs = require('fs');

// Следить за изменениями файла
const watcher = fs.watch('config.json', (eventType, filename) => {
  console.log(`${eventType}: ${filename}`);
  // rename: config.json   (создание/удаление/переименование)
  // change: config.json   (изменение содержимого)
});

// Следить за директорией (рекурсивно на macOS/Windows)
fs.watch('src', { recursive: true }, (event, filename) => {
  console.log(`${event}: ${filename}`);
});

// Остановить наблюдение
watcher.close();

// fs/promises вариант (Node.js 19+)
const { watch } = require('fs/promises');

async function watchDir() {
  const watcher = watch('src', { recursive: true });
  for await (const event of watcher) {
    console.log(`${event.eventType}: ${event.filename}`);
  }
}

fs/promises — полный пример

const fs = require('fs/promises');
const path = require('path');

async function organizeFiles(dir) {
  const entries = await fs.readdir(dir, { withFileTypes: true });

  for (const entry of entries) {
    if (!entry.isFile) continue;

    const ext = path.extname(entry.name).slice(1).toLowerCase();
    if (!ext) continue;

    const targetDir = path.join(dir, ext);
    await fs.mkdir(targetDir, { recursive: true });

    const src = path.join(dir, entry.name);
    const dest = path.join(targetDir, entry.name);
    await fs.rename(src, dest);

    console.log(`${entry.name}${ext}/`);
  }
}

// Раскладывает файлы по папкам-расширениям:
// report.pdf → pdf/report.pdf
// photo.jpg  → jpg/photo.jpg
organizeFiles('./downloads').catch(console.error);

Работа с путями

const fs = require('fs/promises');
const path = require('path');

// ❌ Плохо: относительные пути зависят от cwd
await fs.readFile('config.json');
// Работает только если node запущен из директории с файлом

// ✅ Хорошо: абсолютный путь от текущего модуля
// CommonJS:
const configPath = path.join(__dirname, 'config.json');
await fs.readFile(configPath);

// ESM:
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const configPath2 = path.join(__dirname, 'config.json');

Частые ошибки

  1. readFileSync в обработчике HTTP — блокирует весь сервер, используй await readFile или Streams
  2. Не обрабатывают ENOENT — файл может не существовать, всегда ловите ошибку
  3. Относительные путиfs.readFile('data.txt') зависит от process.cwd, не от расположения файла
  4. writeFile без recursive mkdirwriteFile('data/output.txt') упадёт если data/ не существует
  5. Забывают encoding — без 'utf-8' readFile возвращает Buffer, не строку
  6. Race condition — проверка existsreadFile ненадёжна, файл может исчезнуть между вызовами

Практика

  1. Прочитать JSON-файл, добавить поле, записать обратно (fs/promises)
  2. Написать функцию, рекурсивно считающую общий размер директории
  3. Создать простой логгер: log(message) дописывает строку с timestamp в файл
  4. Реализовать watch на директорию src/, при изменении .js-файла — выводить имя
  5. Написать скрипт, раскладывающий файлы по папкам-расширениям

Связанные темы

  • path — построение кроссплатформенных путей
  • Stream API -- потоки — потоковое чтение/запись больших файлов
  • process — process.cwd и рабочая директория

Ресурсы


🎓 Источник: Работа с файлами, буферами и файловыми потоками в Node.js

  • 📅 2018-10-10 · YouTube · Marp
  • Тезисы:
    • В fs три группы функций: sync, async-callback, promises namespace — одна и та же функциональность с тремя сигнатурами
    • Кодировку utf-8 указывать обязательно: дефолт не везде UTF-8, нужна кроссплатформенность
    • readFile/readFileSync без encoding возвращает Buffer, нужен .toString() или явный encoding
    • error-first контракт: первое что делаем в callback — обрабатываем ошибку, потом трогаем данные
    • Порядок завершения чтения нескольких файлов непредсказуем — нужен индекс из замыкания для сопоставления
    • Имена функций в stack trace: вынесенные именованные функции читаются лучше анонимных
  • Цитата:

    «Как только мы попали в эту функцию, первое что мы должны сделать — обработать ошибку. Мы не должны сразу обращаться к идентификатору buffer.»


🎓 Источник: Архив 2018 — Часть 8 Типизированные массивы, буферы, итераторы, генераторы

  • 📅 2020-01-06 · YouTube · bFT7VGFfP7o
  • Тезисы:
    • Sync — только для старта/конфига; иначе блокирует event loop
    • Базовый конфиг через readFileSync, остальное async параллельно — быстрее стартует
    • Большие файлы читаются буферами (~4КБ), маленькие — за одно чтение
    • forEach запускает чтения параллельно — вывод перемешивается