Модуль 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');
Частые ошибки
- readFileSync в обработчике HTTP — блокирует весь сервер, используй
await readFileили Streams - Не обрабатывают ENOENT — файл может не существовать, всегда ловите ошибку
- Относительные пути —
fs.readFile('data.txt')зависит отprocess.cwd, не от расположения файла - writeFile без recursive mkdir —
writeFile('data/output.txt')упадёт еслиdata/не существует - Забывают encoding — без
'utf-8'readFile возвращает Buffer, не строку - Race condition — проверка
exists→readFileненадёжна, файл может исчезнуть между вызовами
Практика
- Прочитать JSON-файл, добавить поле, записать обратно (fs/promises)
- Написать функцию, рекурсивно считающую общий размер директории
- Создать простой логгер:
log(message)дописывает строку с timestamp в файл - Реализовать watch на директорию src/, при изменении .js-файла — выводить имя
- Написать скрипт, раскладывающий файлы по папкам-расширениям
Связанные темы
- 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 запускает чтения параллельно — вывод перемешивается