Как использовать файловую систему в Node.js

Источник: «How to use the File System in Node.js»
Веб-приложениям не всегда нужна запись в файловую систему, но Node.js предоставляет для этого обширный интерфейс прикладного программирования (API). Это может понадобиться, если вы ведёте логи отладки, передаёте файлы на сервер или с сервера, или создаёте инструменты командной строки.

Чтение и запись файлов из кода необязательно сложны, но ваше приложение будет более надёжным, если вы сделаете следующее:

  1. Убедитесь в кроссплатформенности

    Windows, macOS и Linux работают с файлами по-разному. Например, для разделения каталогов в macOS и Linux используется прямой слэш /, а в Windows — обратный слэш \ и запрещены некоторые символы имён файлов, такие как : и ?.

  2. Проверите все дважды!

    Пользователи или другие приложения могут удалить файл или изменить разрешения доступа. Всегда проверяйте наличие таких проблем, и эффективно устраняйте ошибки.

Модуль Node.js fs

Модуль Node.js fs предоставляет методы для управления файлами и каталогами. Если вы используете другие среды исполнения JavaScript:

Все программы JavaScript выполняются в одном потоке обработки. Базовая операционная система обрабатывает такие операции, как чтение и запись файлов, поэтому программа JavaScript продолжает выполняться параллельно. ОС оповещает среду выполнения о завершении операции с файлом.

В документации по fs приводится длинный список функций, но есть три общих типа с похожими функциями, которые мы рассмотрим далее.

1. Функции обратного вызова

Эти функции принимают в качестве аргумента функцию обратного вызова. В следующем примере передаётся встроенная функция, которая выводит содержимое файла myfile.txt. При условии отсутствия ошибок его содержимое отображается в консоли после end of program:

import { readFile } from 'node:fs';

readFile('myfile.txt', { encoding: 'utf8' }, (err, content) => {
if (!err) {
console.log(content);
}
});

console.log('end of program');

Примечание: параметр { encoding: 'utf8' } гаран_тирует, что Node.js вернёт строку текстового содержимого, а не объект Buffer с двоичными данными.

Это становится сложным, когда вам нужно запускать одну функцию за другой и попадать в ад вложенных обратных вызовов! Также легко написать функции обратного вызова, выглядящие корректно, но вызывающие утечки памяти, которые трудно отладить.

В большинстве случаев использование обратных вызовов не имеет смысла. Лишь немногие из приведённых ниже примеров используют их.

2. Синхронные функции

Функции "Sync" эффективно игнорируют неблокирующий ввод/вывод Node и предоставляют синхронные API, как в других языках программирования. Следующий пример выводит содержимое файла myfile.txt до того, как в консоли появится сообщение end of program:

import { readFileSync } from 'node:fs';

try {
const content = readFileSync('myfile.txt', { encoding: 'utf8' });
console.log(content);
}
catch {}

console.log('end of program');

Это выглядит проще, и я никогда бы не сказал, что не стоит использовать Sync…. но, эм… не используйте Sync! Она останавливает цикл событий и приостанавливает работу приложения. Это может быть нормально в CLI-программе при загрузке небольшого файла инициализации, но подумайте о веб-приложении Node.js со 100 одновременными пользователями. Если один пользователь запросит файл, загрузка которого займёт одну секунду, он будет ждать ответа одну секунду — как и все остальные 99 пользователей!

Нет причин использовать синхронные методы, когда у нас есть promise функции.

3. Promise функции

В ES6/2015 были представлены promise. Они представляют собой синтаксический сахар для обратных вызовов, обеспечивающий более сладкий и простой синтаксис, особенно при использовании с async/await. В Node.js также появился API fs/promises, который выглядит и ведёт себя аналогично синтаксису синхронных функций, но остаётся асинхронным:

import { readFile } from 'node:fs/promises';

try {
const content = await readFile('myfile.txt', { encoding: 'utf8' });
console.log(content);
}
catch {}

console.log('end of program');

Обратите внимание на использование модуля node:fs/promises и await перед readFile().

В большинстве примеров ниже используется синтаксис, основанный на промисах. Большинство из них не включают try и catch для краткости, но вы должны добавить эти блоки для обработки ошибок.

Синтаксис модуля ES

В примерах этого руководства также используется import ES Modules (ESM), а не CommonJS require. ESM — это стандартный синтаксис модулей, поддерживаемый в Deno, Bun и браузерных runtimes.

Чтобы использовать ESM в Node.js, либо:

Вы все ещё можете использовать CommonJS require, если это необходимо.

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

Существует несколько функций для чтения файлов, но самая простая — это чтение всего файла в память с помощью readFile, как мы видели в примере выше:

import { readFile } from 'node:fs/promises';
const content = await readFile('myfile.txt', { encoding: 'utf8' });

Второй объект options также может быть строкой. Он задаёт кодировку (encoding): установите utf8 или другой текстовый формат для чтения содержимого файла в строку.

В качестве альтернативы можно читать строки по одной, используя метод readLines() объекта filehandle:

import { open } from 'node:fs/promises';

const file = await open('myfile.txt');

for await (const line of file.readLines()) {
console.log(line);
}

Есть также более продвинутые опции для чтения потоков или любого количества байт из файла.

Работа с файлами и путями каталогов

Вы часто хотите получить доступ к файлам по определённым абсолютным путям или путям относительно рабочей директории Node приложения. Модуль node:path предоставляет кроссплатформенные методы для разрешения путей во всех операционных системах.

Свойство path.sep возвращает символ разделителя каталогов — \ в Windows или / в Linux или macOS:

import * as path from 'node:path';

console.log( path.sep );

Но есть и более полезные свойства и функции. join([...paths]) объединяет все сегменты пути и нормализует для ОС:

console.log( path.join('/project', 'node/example1', '../example2', 'myfile.txt') );
/*
/project/node/example2/myfile.txt на macOS/Linux
\project\node\example2\myfile.txt на Windows
*/

resolve([...paths]) аналогичен, но возвращает полный абсолютный путь:

console.log( path.resolve('/project', 'node/example1', '../example2', 'myfile.txt') );
/*
/project/node/example2/myfile.txt на macOS/Linux
C:\project\node\example2\myfile.txt на Windows
*/

normalize(path) разрешает все каталоги .. и . ссылок:

console.log( path.normalize('/project/node/example1/../example2/myfile.txt') );
/*
/project/node/example2/myfile.txt на macOS/Linux
\project\node\example2\myfile.txt на Windows
*/

relative(from, to) вычисляет относительный путь между двумя абсолютными или относительными путями (на основе рабочего каталога Node):

console.log( path.relative('/project/node/example1', '/project/node/example2') );
/*
../example2 на macOS/Linux
..\example2 на Windows
*/

'format(object)' строит полный путь из объекта, состоящего из составных частей:

console.log(
path.format({
dir: '/project/node/example2',
name: 'myfile',
ext: 'txt'
})
);
/*
/project/node/example2/myfile.txt
*/

parse(path) делает обратное и возвращает объект, описывающий путь:

console.log( path.parse('/project/node/example2/myfile.txt') );
/*
{
root: '/',
dir: '/project/node/example2',
base: 'myfile.txt',
ext: '.txt',
name: 'myfile'
}
*/

Получение информации о файле и директории

Часто требуется получить информацию о пути. Является ли он файлом? Является ли он каталогом? Когда он был создан? Когда он был последний раз изменён? Можете ли вы прочитать его? Можете ли вы добавлять в него данные?

Функция stat(path) возвращает объект Stats, содержащий информацию об объекте файла или каталога:

import { stat } from 'node:fs/promises';

const info = await stat('myfile.txt');
console.log(info);
/*
Stats {
dev: 4238105234,
mode: 33206,
nlink: 1,
uid: 0,
gid: 0,
rdev: 0,
blksize: 4096,
ino: 3377699720670299,
size: 21,
blocks: 0,
atimeMs: 1700836734386.4246,
mtimeMs: 1700836709109.3108,
ctimeMs: 1700836709109.3108,
birthtimeMs: 1700836699277.3362,
atime: 2023-11-24T14:38:54.386Z,
mtime: 2023-11-24T14:38:29.109Z,
ctime: 2023-11-24T14:38:29.109Z,
birthtime: 2023-11-24T14:38:19.277Z
}
*/

Кроме того, в нем представлены полезные методы, в том числе:

const isFile = info.isFile(); // true
const isDirectory = info.isDirectory(); // false

Функция access(path) проверяет, можно ли получить доступ к файлу, используя определённый режим, заданный через constants. Если проверка доступности прошла успешно, промис выполняется без значения. При неудаче обещание отклоняется. Например:

import { access, constants } from 'node:fs/promises';

const info = {
canRead: false,
canWrite: false,
canExec: false
};

// is readable?
try {
await access('myfile.txt', constants.R_OK);
info.canRead = true;
}
catch {}

// is writeable
try {
await access('myfile.txt', constants.W_OK);
info.canWrite = true;
}
catch {}

console.log(info);
/*
{
canRead: true,
canWrite: true
}
*/

Вы можете проверить более одного режима, например, проверить, доступен ли файл для чтения и записи:

await access('myfile.txt', constants.R_OK | constants.W_OK);

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

writeFile() — простейшая функция для асинхронной записи целого файла с заменой его содержимого, если он уже существует:

import { writeFile } from 'node:fs/promises';
await writeFile('myfile.txt', 'new file contents');

Передайте следующие аргументы:

Аналогичная функция appendFile() добавляет новое содержимое в конец текущего файла, создавая его, если он не существует.

Для самых смелых есть метод filehandler.write(), позволяющий заменить содержимое файла в определённой точке и с определённой длиной.

Создание директорий

Функция mkdir() может создавать полные структуры каталогов, получая абсолютный или относительный путь:

import { mkdir } from 'node:fs/promises';

await mkdir('./subdir/temp', { recursive: true });

Вы можете передать два аргумента:

Установка значения recursive в true создаёт всю структуру каталогов. В приведённом примере subdir создаётся в текущем рабочем каталоге, а temp — как подкаталог этого каталога. Если бы значение recursive было false (по умолчанию), promise было бы отклонён, если бы subdir не был уже определён.

mode — это разрешение для пользователей, групп и остальных в macOS/Linux со значением по умолчанию 0x777. В Windows это не поддерживается и игнорируется.

Аналогичная функция .mkdtemp() создаёт уникальный каталог, обычно предназначенный для временного хранения данных.

Чтение содержимого каталога

.readdir() считывает содержимое каталога. Promise выполняется с массивом, содержащим все имена файлов и каталогов (кроме . и ..). Имя указывается относительно каталога и не включает полный путь:

import { readdir } from 'node:fs/promises';

const files = await readdir('./'); // current working directory
for (const file of files) {
console.log(file);
}

/*
file1.txt
file2.txt
file3.txt
index.mjs
*/

Вы можете передать опциональный второй параметр-объект со следующими свойствами:

Альтернативная функция .opendir() позволяет асинхронно открыть каталог для итеративного сканирования:

import { opendir } from 'node:fs/promises';

const dir = await opendir('./');
for await (const entry of dir) {
console.log(entry.name);
}

Удаление файлов и директорий

Функция .rm() удаляет файл или директорию по указанному пути:

import { rm } from 'node:fs/promises';

await rm('./oldfile.txt');

Вы можете передать в качестве необязательного второго параметра объект со следующими свойствами:

Похожая функция .rmdir() удаляет только каталоги (нельзя передавать путь к файлу). Подобным образом .unlink() удаляет только файлы или символические ссылки (нельзя передавать путь к каталогу).

Прочие функции файловой системы

Приведённые выше примеры иллюстрируют наиболее часто используемые опции для чтения, записи, обновления и удаления файлов и каталогов. Node.js также предоставляет дополнительные, менее используемые опции, такие как копирование, переименование, изменение владельца, изменение разрешений, изменение свойств даты, создание символических ссылок и наблюдение за изменениями файлов.

Возможно, при отслеживании изменений файлов предпочтительнее использовать API на основе обратных вызовов, поскольку в нем меньше кода, он проще в использовании и не может останавливать другие процессы:

import { watch } from 'node:fs';

// запуск обратного вызова, когда в каталоге что-то изменяется
watch('./mydir', { recursive: true }, (event, file) => {

console.log(`event: ${ event }`);

if (file) {
console.log(`file changed: ${ file }`);
}

});

// сделать что-то еще...

Параметр event, получаемый обратным вызовом, — это либо change, либо rename.

Подведение итогов

Node.js предоставляет гибкий и кроссплатформенный API для управления файлами и каталогами в любой операционной системе, где можно использовать среду выполнения. Немного усилий, и вы сможете писать надёжный и переносимый JavaScript-код, способный взаимодействовать с любой файловой системой.

Для получения дополнительной информации обратитесь к документации по Node.js fs и path. Другие полезные библиотеки включают:

Вы также можете найти модули файловой системы более высокого уровня на npm, но нет лучшего опыта, чем написать свой собственный модуль.

Часто задаваемые вопросы о доступе к файловой системе в Node.js

Что такое модуль файловой системы в Node.js

Модуль файловой системы, часто называемый 'fs' — это основной модуль в Node.js, предоставляющий методы и функциональность для взаимодействия с файловой системой, включая чтение и запись файлов.

Как включить модуль fs в сценарий Node.js

Вы можете включить модуль fs с помощью выражения require, например, так: const fs = require('fs');. Это сделает все методы fs доступными в вашем скрипте.

В чем разница между синхронными и асинхронными файловыми операциями в Node.js

Синхронные файловые операции блокируют цикл событий Node.js до завершения операции, в то время как асинхронные операции не блокируют цикл событий, позволяя вашему приложению оставаться отзывчивым. Асинхронные операции обычно рекомендуются для задач ввода/вывода.

Как прочитать содержимое файла в Node.js с помощью модуля fs

Вы можете использовать метод fs.readFile() для чтения содержимого файла. Укажите путь к файлу и функцию обратного вызова для обработки данных после их считывания.

Каково назначение функции обратного вызова при работе с модулем fs в Node.js

Функции обратного вызова в операциях fs используются для обработки результатов асинхронных файловых операций. Они вызываются по завершении операции, передавая в качестве аргументов любые ошибки и данные.

Как проверить, существует ли файл в Node.js, используя модуль fs

Вы можете использовать метод fs.existsSync(), чтобы проверить, существует ли файл по указанному пути. Он возвращает true, если файл существует, и false, если не существует.

Что такое метод fs.createReadStream() и чем он полезен

fs.createReadStream() используется для эффективного чтения больших файлов. Она создаёт читаемый поток для указанного файла, позволяя читать и обрабатывать данные небольшими, управляемыми кусками.

Можно ли использовать модуль fs для создания и записи в новый файл в Node.js

Да, вы можете использовать методы fs.writeFile() или fs.createWriteStream() для создания и записи в новый файл. Эти методы позволяют указать путь к файлу, содержимое и параметры записи.

Как обрабатывать ошибки при работе с модулем fs в Node.js

Вы всегда должны обрабатывать ошибки, проверяя параметр ошибки в функции обратного вызова, предоставляемой асинхронным методам fs, или используя блоки try/catch для синхронных операций.

Можно ли удалить файл с помощью модуля fs в Node.js

Да, вы можете использовать метод fs.unlink() для удаления файла. Укажите путь к файлу и функцию обратного вызова для обработки результата.

Можно ли использовать модуль fs для работы с каталогами и структурами папок в Node.js

Да, модуль fs предоставляет методы для создания, чтения и работы с каталогами, включая создание каталогов, просмотр их содержимого и удаление каталогов.

Дополнительные материалы

Предыдущая Статья

Почему нам нравятся минимальные сидеры для тестов

Следующая Статья

PHP атрибуты в Laravel