Урок 04

Файловая система

Модуль fs

Работа с файлами и папками.

import fs from 'node:fs';
import fs from 'node:fs/promises';   // промисы

Синхронный vs асинхронный

Синхронный (*Sync):

const data = fs.readFileSync('file.txt', 'utf8');
console.log(data);

Блокирует поток. Для CLI-скриптов — ок. Для сервера — плохо.

Асинхронный (callback):

fs.readFile('file.txt', 'utf8', (err, data) => {
    if (err) {
        console.error(err);
        return;
    }
    console.log(data);
});

Асинхронный (промисы) — современный:

import fs from 'node:fs/promises';

const data = await fs.readFile('file.txt', 'utf8');
console.log(data);

Правило: всегда используй fs/promises.

Чтение файла

import fs from 'node:fs/promises';

// Текст
const text = await fs.readFile('file.txt', 'utf8');

// Бинарные данные (без 'utf8')
const buffer = await fs.readFile('image.png');

Запись файла

await fs.writeFile('output.txt', 'Привет, мир!');
await fs.writeFile('data.json', JSON.stringify(data, null, 2));

Перезаписывает. Если не существует — создаст.

Добавление в файл

await fs.appendFile('log.txt', 'Новая строка\n');

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

try {
    await fs.access('file.txt');
    console.log('Файл есть');
} catch {
    console.log('Файла нет');
}

Не используй existsSync для критичных проверок — между проверкой и использованием файл может исчезнуть.

Лучше — try/catch вокруг операции.

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

const stats = await fs.stat('file.txt');

console.log(stats.size);        // размер в байтах
console.log(stats.isFile());    // файл?
console.log(stats.isDirectory());// папка?
console.log(stats.mtime);       // дата изменения

Удаление

await fs.unlink('file.txt');       // удалить файл
await fs.rmdir('empty-folder');    // удалить пустую папку
await fs.rm('folder', { recursive: true });  // с содержимым

Переименование / перемещение

await fs.rename('old.txt', 'new.txt');
await fs.rename('folder/file.txt', 'other/file.txt');

Копирование

await fs.copyFile('source.txt', 'dest.txt');

Папки

Создать:

await fs.mkdir('new-folder');
await fs.mkdir('a/b/c', { recursive: true });

Прочитать:

const files = await fs.readdir('folder');
// ['file1.txt', 'file2.txt', 'subfolder']

const entries = await fs.readdir('folder', { withFileTypes: true });
entries.forEach(e => {
    if (e.isDirectory()) console.log('DIR:', e.name);
    else console.log('FILE:', e.name);
});

Модуль path

Работа с путями — кроссплатформенно.

import path from 'node:path';

Соединение:

path.join('src', 'utils', 'math.js');
// 'src/utils/math.js' (на Linux)
// 'src\\utils\\math.js' (на Windows)

Абсолютный путь:

path.resolve('src', 'index.js');
// '/home/user/project/src/index.js'

Имя и расширение:

path.basename('/a/b/file.txt');       // 'file.txt'
path.basename('/a/b/file.txt', '.txt'); // 'file'
path.extname('/a/b/file.txt');        // '.txt'
path.dirname('/a/b/file.txt');        // '/a/b'

Разбор:

path.parse('/a/b/file.txt');
// { root: '/', dir: '/a/b', base: 'file.txt', ext: '.txt', name: 'file' }

__dirname и __filename

CJS:

console.log(__dirname);   // /home/user/project/src
console.log(__filename);  // /home/user/project/src/index.js

ESM — нет. Аналоги:

import { fileURLToPath } from 'node:url';
import path from 'node:path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

Или (Node.js 20.11+):

console.log(import.meta.dirname);
console.log(import.meta.filename);

Абсолютные пути

Всегда используй:

import path from 'node:path';

const filePath = path.join(process.cwd(), 'data', 'file.json');

process.cwd() — текущая рабочая директория.

Не используй относительные — сломается при запуске из другой папки.

Рекурсивный обход

Node.js 20+:

for await (const entry of fs.glob('**/*.js')) {
    console.log(entry);
}

Или вручную:

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

    for (const entry of entries) {
        const full = path.join(dir, entry.name);

        if (entry.isDirectory()) {
            await walk(full);
        } else {
            console.log(full);
        }
    }
}

Отслеживание изменений

import fs from 'node:fs';

fs.watch('file.txt', (eventType, filename) => {
    console.log(eventType, filename);
});

eventType — 'rename' или 'change'.

Для разработки — chokidar (стабильнее).

Работа с JSON

import fs from 'node:fs/promises';

async function readJson(path) {
    const text = await fs.readFile(path, 'utf8');
    return JSON.parse(text);
}

async function writeJson(path, data) {
    await fs.writeFile(path, JSON.stringify(data, null, 2));
}

Практический пример: логгер

import fs from 'node:fs/promises';
import path from 'node:path';

class Logger {
    constructor(fileName = 'app.log') {
        this.path = path.join(process.cwd(), 'logs', fileName);
    }

    async init() {
        await fs.mkdir(path.dirname(this.path), { recursive: true });
    }

    async log(message) {
        const line = `[${new Date().toISOString()}] ${message}\n`;
        await fs.appendFile(this.path, line);
    }
}

const logger = new Logger();
await logger.init();
await logger.log('Приложение запущено');
await logger.log('Пользователь вошёл');

Практика: чтение конфига

import fs from 'node:fs/promises';
import path from 'node:path';

async function loadConfig(name = 'config.json') {
    const configPath = path.join(process.cwd(), name);

    try {
        const text = await fs.readFile(configPath, 'utf8');
        return JSON.parse(text);
    } catch (err) {
        if (err.code === 'ENOENT') {
            throw new Error(`Файл не найден: ${configPath}`);
        }
        throw err;
    }
}

const config = await loadConfig();
console.log(config);

Коды ошибок

  • ENOENT — файл не найден.
  • EACCES — нет прав.
  • EEXIST — файл существует.
  • ENOTDIR — не директория.
  • EISDIR — это директория.
try {
    await fs.readFile('missing.txt');
} catch (err) {
    if (err.code === 'ENOENT') {
        console.log('Файл не найден');
    }
}

Итог

  • fs/promises — всегда.
  • readFile, writeFile, appendFile — основные.
  • readdir, mkdir, rm — для папок.
  • path.join, path.resolve — пути.
  • process.cwd() — рабочая папка.
  • import.meta.dirname — папка модуля (ESM).
  • Коды ошибок — ENOENT, EACCES.
  • fs.watch — слежение.

Дальше — асинхронность и Event Loop.