Взаимодействие серверного приложения с дисковой подсистемой строится на концепции файловых дескрипторов (File Descriptor) и системных вызовов ядра ОС (open, read, write, close). В отличие от сетевых сокетов, которые в большинстве операционных систем поддерживают подлинно асинхронные неблокирующие уведомления (механизмы epoll в Linux или kqueue в BSD/macOS), дисковый ввод-вывод для регулярных файлов исторически является блокирующим на уровне ядра.
Чтобы не блокировать единственный главный поток исполнения JavaScript, библиотека libuv использует внутренний пул рабочих потоков (libuv Threadpool). При вызове асинхронных методов из модуля node:fs/promises операция передается свободному потоку из пула, а главный поток продолжает обрабатывать сетевые запросы в цикле событий.
Для манипуляции бинарными данными вне управляемой кучи движка V8 рантайт предоставляет специализированный класс Buffer. Буферы представляют собой непрерывные срезы физической памяти, выделяемые напрямую через аллокаторы C++, что исключает накладные расходы сборщика мусора (Garbage Collector) при передаче мегабайтных массивов между файловой системой и сетевым стеком.
При работе с путями и файлами разработчик оперирует тремя фундаментальными абстракциями:
node:): современный стандарт импорта встроенных модулей Node.js, предотвращающий конфликты имен с пакетами в node_modules.node:path): абстракция над различиями файловых систем (разделители / в POSIX против \ в Windows, относительные и абсолютные пути).Buffer): низкоуровневая структура данных, хранящая последовательность байтов (числа от 0 до 255) в фиксированном объеме оперативной памяти.[ JavaScript Main Thread ]
| await fs.readFile('/data/log.txt')
v
[ libuv Threadpool Queue (uv_fs_read) ]
| (Делегирование блокирующего вызова)
v
[ Worker Thread C++ ] ---> [ POSIX Syscall: read(fd) ] ---> [ Дисковый накопитель ]
|
[ Buffer (Raw C++ Memory) ] <--- Копирование байтов ядра ОС <------+
|
v (Разрешение Promise в цикле событий)
[ Promise<Buffer> resolved ] -> Возврат в JavaScriptФункция `path.resolve()` валидирует аргументы, склеивает сегменты и формирует канонический абсолютный путь в файловой системе ОС.
Вызов `fs.readFile()` создает задачу в очереди ввода-вывода libuv и немедленно возвращает промис в главный поток JavaScript.
Свободный рабочий поток пула совершает синхронный системный вызов к ядру ОС, заполняя непрерывный буфер памяти байтами с диска.
Память оборачивается в экземпляр класса `Buffer` (наследник `Uint8Array`), готовый к декодированию в строку или передаче в сокет.
Цикл событий получает сигнал о завершении операции и передает управление в `then`-колбэк или точку выхода `await`.
Бэкенд-сервис на Node.js обрабатывает 500 входящих HTTP-запросов в секунду. Внутри обработчика роута разработчик написал: `const template = fs.readFileSync('./email.html', 'utf-8');`. Что произойдет с производительностью сервера под нагрузкой?
path.resolve и path.relative.secrets.json, .env) на этапе холодного старта контейнера до открытия входящего сетевого сокета.utf-8, base64url и hex с помощью методов Buffer.from() и buf.toString().fs.writeFile с последующим мгновенным переименованием через fs.rename, исключающая повреждение данных при внезапном сбое питания сервера.Начиная с современных версий Node.js, все встроенные системные библиотеки импортируются с явным префиксом node:. Это гарантирует отсутствие конфликтов с пакетами из реестра npm и ускоряет работу модульного резолвера:
// Современный стандарт импорта в 2026 году
import fs from 'node:fs/promises';
import path from 'node:path';
import { Buffer } from 'node:buffer';Разработчики часто совершают ошибку, конкатенируя пути строками (dir + '/' + file). Это приводит к фатальным сбоям на разных операционных системах (Windows использует обратный слеш \, Linux — прямой /).
Модуль node:path предоставляет два фундаментальных метода:
path.join([...paths]): просто соединяет все переданные сегменты с использованием системного разделителя и нормализует результат (удаляет лишние точки . и относительные переходы ..).path.resolve([...paths]): обрабатывает последовательность путей справа налево до тех пор, пока не будет построен абсолютный путь, отталкиваясь от текущей рабочей директории процесса (process.cwd()).import path from 'node:path';
const uploadsDir = path.resolve(process.cwd(), 'storage', 'uploads');
export function getSafeFilePath(userProvidedFilename: string): string {
// 1. Нормализуем абсолютный путь к целевому файлу
const safeTarget = path.resolve(uploadsDir, userProvidedFilename);
// 2. Защита от Directory Traversal (например, "../../etc/passwd")
const relativeDistance = path.relative(uploadsDir, safeTarget);
if (relativeDistance.startsWith('..') || path.isAbsolute(relativeDistance)) {
throw new Error('Access Denied: Path Traversal attempt detected!');
}
return safeTarget;
}Модуль node:fs/promises возвращает стандартные промисы JavaScript, позволяя организовывать чистые цепочки с async/await:
import fs from 'node:fs/promises';
import path from 'node:path';
interface LogEntry {
timestamp: number;
level: 'info' | 'warn' | 'error';
message: string;
}
export async function appendSystemLog(logDir: string, entry: LogEntry): Promise<void> {
// 1. Создаем директорию рекурсивно, если она не существует (аналог mkdir -p)
await fs.mkdir(logDir, { recursive: true });
const filePath = path.join(logDir, 'service.log');
const serialized = JSON.stringify(entry) + '\n';
// 2. Асинхронная дозапись в конец файла (флаг 'a' = append)
await fs.writeFile(filePath, serialized, {
encoding: 'utf-8',
flag: 'a',
mode: 0o640 // Права доступа: rw-r-----
});
}
export async function readBinaryPayload(filePath: string): Promise<Buffer> {
// Чтение без указания кодировки возвращает сырой экземпляр Buffer
const rawBytes: Buffer = await fs.readFile(filePath);
return rawBytes;
}Buffer — это глобальный класс в Node.js, наследующий стандартный интерфейс Uint8Array. Он оперирует сырыми байтами памяти.
Существует три основных метода создания буфера:
Buffer.from(data, encoding): копирует существующие данные (строку, массив, другой буфер) в новую область памяти.Buffer.alloc(size, fill): безопасно выделяет блок памяти заданного размера и гарантированно зануляет его (заполняет нулями 0x00).Buffer.allocUnsafe(size): мгновенно резервирует непрерывный сегмент памяти без очистки. Работает быстрее, но выделенный сегмент может содержать старые фрагменты данных из RAM (утечка чувствительной информации).import { Buffer } from 'node:buffer';
// 1. Создание буфера из UTF-8 строки
const utf8Buffer = Buffer.from('Привет, мир! 🚀', 'utf-8');
console.log('Длина в символах:', 'Привет, мир! 🚀'.length); // 15
console.log('Размер в байтах памяти:', utf8Buffer.byteLength); // 26 (кириллица занимает по 2 байта, эмодзи - 4)
// 2. Кодирование в формат Base64 (для заголовков HTTP или JSON)
const base64String = utf8Buffer.toString('base64');
console.log('Base64 Encoded:', base64String);
// 3. Декодирование обратно из Base64
const restoredBuffer = Buffer.from(base64String, 'base64');
console.log('Restored String:', restoredBuffer.toString('utf-8'));
// 4. Представление в Hex-формате (шестнадцатеричный дамп)
const hexSignature = Buffer.from([0xFF, 0xD8, 0xFF, 0xE0]); // Сигнатура JPEG
console.log('Hex Signature:', hexSignature.toString('hex')); // 'ffd8ffe0'Код приложения вызывает fs.readFile(). Создается объект запроса ввода-вывода (uv_fs_t) с файловым дескриптором.
Главный поток JavaScript освобождается мгновенно.
Запрос помещается в системную очередь задач ввода-вывода подсистемы libuv.
Размер пула регулируется переменной UV_THREADPOOL_SIZE.
Свободный рабочий поток извлекает задачу, совершает блокирующий вызов read() к диску и заполняет буфер памяти.
Ядро ОС передает данные напрямую через DMA контроллер.
Воркер сигнализирует главному циклу событий о завершении. Создается экземпляр Buffer и вызывается resolve() промиса.
Результат готов к дальнейшей обработке в JavaScript.
Разработчик сервиса генерации превью создает буфер: `const buf = Buffer.allocUnsafe(1024);`. После этого он записывает в буфер только первые 200 байт изображения и сразу отправляет весь буфер клиенту по HTTP: `res.end(buf);`. Какая критическая уязвимость безопасности здесь возникает?
Прямая склейка пользовательского ввода с базовым каталогом (uploadsDir + '/' + req.query.file) позволяет злоумышленнику передать значение вида ../../../../etc/passwd или ../../.env. Это приведет к тому, что сервер прочитает конфиденциальный файл за пределами целевой папки. Для предотвращения атаки обязательно проверяйте путь через path.resolve() и path.relative(), как показано в примере выше.
Попытка прочитать видеофайл или дамп базы данных размером в 3 ГБ с помощью await fs.readFile('huge-video.mp4') приведет к мгновенному исчерпанию лимита кучи V8 (RangeError [ERR_FS_FILE_TOO_LARGE] или JavaScript heap out of memory). Файлы, превышающие размер в несколько мегабайт, должны обрабатываться исключительно потоками (node:stream) с использованием fs.createReadStream().
Свойство строки 'Строка'.length возвращает количество символов UTF-16 (6 символов). Однако в кодировке UTF-8 кириллические символы кодируются двумя байтами каждый. Буфер Buffer.from('Строка') будет иметь byteLength === 12. Попытка выставить HTTP-заголовок Content-Length: str.length приведет к обрыву соединения сокетом браузера из-за несоответствия реального размера передаваемых байтов.