File System
1. Концепции, паттерны и соглашения, используемые в ФС
В данном разделе предполагаются следующие импорты:
import * as fs from "node:fs";
import * as fsPromises from "node:fs/promises";
1.2. Стиль функций
ФС предоставляет 3 стиля функций:
- синхронный стиль с обычными функциями:
fs.readFileSync(path, options?): string | Buffer
;
- 2 асинхронных стиля:
- с функциями обратного вызова:
fs.readFile(path, options?, callback): void
;
- с функциями, возвращающими промисы:
fsPromises.readFile(path, options?): Promise<string | Buffer>
.
- с функциями обратного вызова:
1.1.1. Синхронные функции
Синхронные функции являются самыми простыми - они сразу возвращают значения и выбрасывают ошибки в виде исключений:
import * as fs from "node:fs";
try {
const result = fs.readFileSync("/etc/passwd", { encoding: "utf-8" });
console.log(result);
} catch (err) {
console.error(err);
}
В статье, в основном, используется данный стиль.
1.1.2. Функции, основанные на промисах
Такие функции возвращают промисы, которые разрешаются результатами и отклоняются с ошибками:
import * as fsPromises from "node:fs/promises";
try {
const result = await fsPromises.readFile(
"/etc/passwd", { encoding: "utf-8" });
console.log(result);
} catch (err) {
console.error(err);
}
Обратите внимание: промисифицированный (promisified) ФС импортируется из другого модуля.
1.1.3. Функции, основанные на колбэках
Такие функции передают результат и ошибки колбэку, передаваемому им в качестве последнего аргумента:
import * as fs from "node:fs";
fs.readFile("/etc/passwd", { encoding: "utf-8" },
(err, result) => {
if (err) {
console.error(err);
return;
}
console.log(result);
}
);
Данный стиль в статье не используется (он считается устаревшим).
1.2. Доступ к файлам
- все содержимое файла можно читать и записывать в виде строки;
- потоки для чтения и записи позволяют обрабатывать файлы небольшими частями (чанками/chunks), по одной за раз. Потоки разрешают только последовательный доступ;
- для последовательного и произвольного доступа могут использоваться файловые дескрипторы или
FileHandles
, отдаленно напоминающие потоки:- файловые дескрипторы - это целые числа, представляющие файлы. Они управляются с помощью следующих функций (у каждой синхронной версии имеется колбэк-эквивалент -
fs.open()
и т.п.):fs.openSync(path, flags?, mode?)
: открывает новый дескриптор файла по указанному пути и возвращает его;fs.closeSync(fd)
: закрывает дескриптор;fs.fchmodSync(fd, mode)
;fs.fchownSync(fd, uid, gid)
;fs.fdatasyncSync(fd)
;fs.fstatSync(fd, options?)
;fs.fsyncSync(fd)
;fs.ftruncateSync(fd, len?)
;fs.futimesSync(fd, atime, mtime)
;
- файловые дескрипторы могут использоваться синхронным и колбэк-ФС. Промис-ФС предоставляет абстракцию - класс FileHandle, основанный на дескрипторах. Экземпляры создаются с помощью
fsPromises.open()
. Операции выполняются с помощью таких методов (не функций), как: fileHandle.close()
;fileHandle.chmod(mode)
;fileHandle.chown(uid, gid)
;- и др.
- файловые дескрипторы - это целые числа, представляющие файлы. Они управляются с помощью следующих функций (у каждой синхронной версии имеется колбэк-эквивалент -
FileHandles
в этой статье не рассматриваются.
1.3. Префиксы названий функций
1.3.1. Префикс "l": символические ссылки
Функции, названия которых начинаются с буквы l
, как правило, оперируют символическими ссылками:
fs.lchmodSync()
,fs.lchmod()
,fsPromises.lchmod()
;fs.lchownSync()
,fs.lchown()
,fsPromises.lchown()
;fs.lutimesSync()
,fs.lutimes()
,fsPromises.lutimes()
;- и др.
1.3.2. Префикс "f": дескрипторы файлов
Функции, названия которых начинаются с буквы f
, как правило, управляют файловыми дескрипторами:
fs.fchmodSync()
,fs.fchmod()
;fs.fchownSync()
,fs.fchown()
;fs.fstatSync()
,fs.fstat()
;- и др.
1.4. Важные классы
1.4.1. URL
: альтернатива строковым путям к файловой системе
Функции, принимающие строковые пути (1), как правило, также принимают экземпляры URL (2):
import * as fs from "node:fs";
assert.equal(
fs.readFileSync(
"/tmp/data.txt", { encoding: "utf-8" }), // (1)
"Текст"
);
assert.equal(
fs.readFileSync(
new URL("file:///tmp/data.txt"), { encoding: "utf-8" }), // (2)
"Текст"
);
Ручное преобразование путей в file:
кажется простым, но необходимо учитывать большое количество нюансов: процентное кодирование и декодирование, управляющие символы, буквы дисков Windows
и т.д. Поэтому лучше применять следующие функции:
URL
будет рассмотрен в одной из следующих статей.
1.4.2. Буферы
Класс Buffer представляет последовательность байтов фиксированного размера. Он является подклассом Uint8Array (TypedArray - типизированного массива). Буферы используются, в основном, для работы с файлами, содержащими бинарные данные.
Функции, принимающие Buffer
, также принимают Uint8Array
. Поскольку Uint8Arrays
являются кроссплатформенными, а Buffers
нет, предпочтение следует отдавать первым.
Преимущество буферов состоит в возможности кодирования и декодирования текста в разные кодировки. Для кодирования или декодирования UTF-8
в Uint8Array
можно использовать TextEncoder или TextDecoder. Эти классы доступны на большинстве JavaScript-платформ
:
> new TextEncoder().encode("café")
Uint8Array.of(99, 97, 102, 195, 169)
> new TextDecoder().decode(Uint8Array.of(99, 97, 102, 195, 169))
"café"
1.4.3. Потоки
Некоторые функции принимают или возвращают нативные потоки данных (native streams):
stream.Readable
: класс для создания потоков для чтения. Модульnode:fs
используетfs.ReadStream
, который является подклассомstream.Readable
;stream.Writable
: класс для создания потоков для записи. Модульnode:fs
используетfs.WriteStream
, который является подклассомstream.Writable
.
Вместо нативных потоков можно использовать кроссплатформенные веб-потоки (web streams), о которых рассказывалось в одной из предыдущих статей.
2. Чтение и запись файлов
2.1. Синхронное чтение файла в строку (опционально: разбиение по строкам)
fs.readFileSync(path, options?) читает файл по указанному пути в строку (результат чтения файла возвращается в виде единой строки):
import * as fs from "node:fs";
assert.equal(
fs.readFileSync("data.txt", { encoding: "utf-8" }),
"несколько\r\nстрок\nтекста"
);
Плюсы и минусы данного подхода (по сравнению с использованием потока):
+
: файл читается синхронно, делается это легко. Подходит для многих случаев;-
: плохой выбор для больших файлов - обработке файла предшествует чтение всего содержимого файла.
2.1.1. Разбиение текста без включения разделителей строк
Следующий код разбивает текст построчно и удаляет разделители строк (line terminators):
const RE_EOL = /\r?\n/;
const splitLines = (str) => str.split(RE_EOL);
assert.deepEqual(
splitLines("несколько\r\nстрок\nтекста"),
["несколько", "строк", "текста"]
);
"EOL" расшифровывается как "end of line" (конец строки).
2.1.2. Разбиение текста с включением разделителей строк
const RE_EOL = /(?<=\r?\n)/; // (1)
const splitLinesWithEols = (str) => str.split(RE_EOL);
assert.deepEqual(
splitLinesWithEols("несколько\r\nстрок\nтекста"),
["несколько\r\n", "строк\n", "текста"]
);
assert.deepEqual(
splitLinesWithEols("первый\n\nтретий"),
["первый\n", "\n", "третий"]
);
assert.deepEqual(
splitLinesWithEols("EOL в конце\n"),
["EOL в конце\n"]
);
assert.deepEqual(
splitLinesWithEols(""),
[""]
);
На строке 1 у нас имеется регулярное выражение с ретроспективной проверкой (lookbehind assertion). Оно сопоставляется с тем, что предшествует \r?\n
, но ничего не захватывает. Поэтому разделители сохраняются.
На платформах, не поддерживающих ретроспективные проверки, можно использовать такую функцию:
function splitLinesWithEols(str) {
if (str.length === 0) return [""];
const lines = [];
let prevEnd = 0;
while (prevEnd < str.length) {
// Поиск "\n" также означает поиск "\r\n"
const newlineIndex = str.indexOf("\n", prevEnd);
// Перевод на новую строку включается в строку
const end = newlineIndex < 0 ? str.length : newlineIndex + 1;
lines.push(str.slice(prevEnd, end));
prevEnd = end;
}
return lines;
}
2.2. Построчное чтение файла с помощью потока
import * as fs from "node:fs";
import { Readable } from "node:stream";
const nodeReadable = fs.createReadStream(
"text-file.txt",
{ encoding: "utf-8" }
);
const webReadableStream = Readable.toWeb(nodeReadable);
const lineStream = webReadableStream.pipeThrough(new ChunksToLinesStream());
for await (const line of lineStream) {
console.log(line);
}
/**
* несколько\r\n
* строк\n
* текста
*/
Вот, что здесь используется:
- fs.createReadStream(path, options?): создает поток (экземпляр
stream.Readable
); - stream.Readable.toWeb(nodeReadable): преобразует доступный для чтения поток
Node.js
в веб-поток (экземплярReadableStream
); - класс ChunksToLinesStream представляет поток для преобразования. Чанки - это небольшие части данных, производимые потоками. Если у нас есть поток, чанки которого представляют строки произвольной длины, и мы пропускает эти чанки через
ChunksToLinesStream
, то получаем поток с построчными чанками.
Веб-потоки являются асинхронно итерируемыми, что позволяет использовать цикл for-await-of
для их перебора.
Плюсы и минусы данного подхода (по сравнению с чтением в строку):
+
: хорошо подходит для больших файлов - данные могут обрабатываться инкрементально, не нужно ждать завершения чтения всего содержимого файла;-
: данные читаются асинхронно, код сложнее и его больше.
2.3. Синхронная запись строки в файл
fs.writeFileSync(path, str, options?) записывает строку в файл по указанному пути. Сущест вующий файл перезаписывается:
import * as fs from "node:fs";
fs.writeFileSync(
"data.txt",
"Первая строка\nВторая строка\n",
{ encoding: "utf-8" }
);
Плюсы и минусы (по сравнению с потоком):
+
: файл записывается синхронно, делается это легко. Подходит для многих случаев;-
: плохой выбор для больших файлов.