Node.js: работа с файловой системой
- вторник, 26 июля 2022 г. в 00:44:22
Привет, друзья!
Представляю вашему вниманию перевод этой замечательной статьи.
Данная статья включает в себя:
интерфейса Node.js
для работы с файловой системой (далее — ФС);Если вам это интересно, прошу под кат.
1.4.1. URL
: альтернатива строковым путям к файловой системеВ данном разделе предполагаются следующие импорты:
import * as fs from "node:fs";
import * as fsPromises from "node:fs/promises";
ФС предоставляет 3 стиля функций:
fs.readFileSync(path, options?): string | Buffer
;fs.readFile(path, options?, callback): void
;fsPromises.readFile(path, options?): Promise<string | Buffer>
.Синхронные функции являются самыми простыми — они сразу возвращают значения и выбрасывают ошибки в виде исключений:
import * as fs from "node:fs";
try {
const result = fs.readFileSync("/etc/passwd", { encoding: "utf-8" });
console.log(result);
} catch (err) {
console.error(err);
}
В статье, в основном, используется данный стиль.
Такие функции возвращают промисы, которые разрешаются результатами и отклоняются с ошибками:
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) ФС импортируется из другого модуля.
Такие функции передают результат и ошибки колбэку, передаваемому им в качестве последнего аргумента:
import * as fs from "node:fs";
fs.readFile("/etc/passwd", { encoding: "utf-8" },
(err, result) => {
if (err) {
console.error(err);
return;
}
console.log(result);
}
);
Данный стиль в статье не используется (он считается устаревшим).
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)
;fsPromises.open()
. Операции выполняются с помощью таких методов (не функций), как:fileHandle.close()
;fileHandle.chmod(mode)
;fileHandle.chown(uid, gid)
;FileHandles
в этой статье не рассматриваются.
Функции, названия которых начинаются с буквы l
, как правило, оперируют символическими ссылками:
fs.lchmodSync()
, fs.lchmod()
, fsPromises.lchmod()
;fs.lchownSync()
, fs.lchown()
, fsPromises.lchown()
;fs.lutimesSync()
, fs.lutimes()
, fsPromises.lutimes()
;Функции, названия которых начинаются с буквы f
, как правило, управляют файловыми дескрипторами:
fs.fchmodSync()
, fs.fchmod()
;fs.fchownSync()
, fs.fchown()
;fs.fstatSync()
, fs.fstat()
;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
будет рассмотрен в одной из следующих статей.
Класс 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é"
Некоторые функции принимают или возвращают нативные потоки данных (native streams):
stream.Readable
: класс для создания потоков для чтения. Модуль node:fs
использует fs.ReadStream
, который является подклассом stream.Readable
;stream.Writable
: класс для создания потоков для записи. Модуль node:fs
использует fs.WriteStream
, который является подклассом stream.Writable
.Вместо нативных потоков можно использовать кроссплатформенные веб-потоки (web streams), о которых рассказывалось в одной из предыдущих статей.
fs.readFileSync(path, options?) читает файл по указанному пути в строку (результат чтения файла возвращается в виде единой строки):
import * as fs from "node:fs";
assert.equal(
fs.readFileSync("data.txt", { encoding: "utf-8" }),
"несколько\r\nстрок\nтекста"
);
Плюсы и минусы данного подхода (по сравнению с использованием потока):
+
: файл читается синхронно, делается это легко. Подходит для многих случаев;-
: плохой выбор для больших файлов — обработке файла предшествует чтение всего содержимого файла.Следующий код разбивает текст построчно и удаляет разделители строк (line terminators):
const RE_EOL = /\r?\n/;
const splitLines = (str) => str.split(RE_EOL);
assert.deepEqual(
splitLines("несколько\r\nстрок\nтекста"),
["несколько", "строк", "текста"]
);
"EOL" расшифровывается как "end of line" (конец строки).
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;
}
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
* текста
*/
Вот, что здесь используется:
stream.Readable
);Node.js
в веб-поток (экземпляр ReadableStream
);ChunksToLinesStream
, то получаем поток с построчными чанками.Веб-потоки являются асинхронно итерируемыми, что позволяет использовать цикл for-await-of
для их перебора.
Плюсы и минусы данного подхода (по сравнению с чтением в строку):
+
: хорошо подходит для больших файлов — данные могут обрабатываться инкрементально, не нужно ждать завершения чтения всего содержимого файла;-
: данные читаются асинхронно, код сложнее и его больше.fs.writeFileSync(path, str, options?) записывает строку в файл по указанному пути. Существующий файл перезаписывается:
import * as fs from "node:fs";
fs.writeFileSync(
"data.txt",
"Первая строка\nВторая строка\n",
{ encoding: "utf-8" }
);
Плюсы и минусы (по сравнению с потоком):
+
: файл записывается синхронно, делается это легко. Подходит для многих случаев;-
: плохой выбор для больших файлов.import * as fs from "node:fs";
fs.writeFileSync(
"data.txt",
"Новая строка\n",
{ encoding: "utf-8", flag: "a" }
);
Настройка flag
со значением a
означает, что мы добавляем данные. Другие возможные значения этой настройки.
Обратите внимание: в одних функциях настройка называется flag
, в других — flags
.
import * as fs from "node:fs";
import { Writable } from "node:stream";
const nodeWritable = fs.createWriteStream(
"data.txt",
{ encoding: "utf-8" }
);
const webWritableStream = Writable.toWeb(nodeWritable);
const writer = webWritableStream.getWriter();
try {
await writer.write("Первая строка\n");
await writer.write("Вторая строка\n");
await writer.close();
} finally {
writer.releaseLock()
}
Вот, что здесь используется:
stream.Writable
);Node.js
в веб-поток.Плюсы и минусы:
+
: хорошо подходит для больших файлов;-
: запись выполняется асинхронно, код сложнее и его больше.import * as fs from "node:fs";
import { Writable } from "node:stream";
const nodeWritable = fs.createWriteStream(
"data.txt",
// !
{ encoding: "utf-8", flags: "a" }
);
const webWritableStream = Writable.toWeb(nodeWritable);
const writer = webWritableStream.getWriter();
try {
await writer.write("Первая добавленная строка\n");
await writer.write("Вторая добавленная строка\n");
await writer.close();
} finally {
writer.releaseLock()
}
На разных платформах используются разные разделители строк, отмечающие конец строки:
Windows
— это \r\n
;Unix
— \n
.Для кроссплатформенной обработки EOL можно использовать несколько стратегий.
При обработке строк с EOL
иногда бывает полезным их удалять:
const RE_EOL_REMOVE = /\r?\n$/;
function removeEol(line) {
const match = RE_EOL_REMOVE.exec(line);
if (!match) return line;
return line.slice(0, match.index);
}
assert.equal(
removeEol("Windows EOL\r\n"),
"Windows EOL"
);
assert.equal(
removeEol("Unix EOL\n"),
"Unix EOL"
);
assert.equal(
removeEol("Без EOL"),
"Без EOL"
);
Для записи разделителей строк в нашем распоряжении имеется 2 варианта:
константа EOL
из модуля node:os
содержит EOL
текущей платформы;EOL
входного файла и использовать этот формат при дальнейшей модификации данного файла.Следующая функция обходит (traverse) директорию и возвращает список всех ее потомков (ее дочерних элементов, потомков дочерних элементов и т.д.):
import * as path from "node:path";
import * as fs from "node:fs";
function* traverseDir(dirPath) {
const dirEntries = fs.readdirSync(dirPath, {withFileTypes: true});
// Сортируем сущности для обеспечения большей предсказуемости
dirEntries.sort(
(a, b) => a.name.localeCompare(b.name, "en")
);
for (const dirEntry of dirEntries) {
const fileName = dirEntry.name;
const pathName = path.join(dirPath, fileName);
yield pathName;
if (dirEntry.isDirectory()) {
yield* traverseDir(pathName);
}
}
}
Здесь:
fs.readdirSync(path, options?)
возвращает потомков директории по указанному пути:withFileTypes
имеет значение true
, функция возвращает записи каталога (directory entries), экземпляры fs.Dirent. Записи каталога содержат такие свойства, как:dirent.name
;dirent.isDirectory()
;dirent.isFile()
;dirent.isSymbolicLink()
;withFileTypes
имеет значение true
или не указана, функция возвращает список названий файлов.Пример использования функции traverseDir
:
for (const filePath of traverseDir("dir")) {
console.log(filePath);
}
/**
* dir/dir-file.txt
* dir/subdir
* dir/subdir/subdir-file1.txt
* dir/subdir/subdir-file2.csv
*/
mkdir
, mkdir -p
)Для создания директорий можно использовать функцию fs.mkdirSync(path, options?).
options.recursive
определяет, как создается директория по указанному пути:
recursive
имеет значение false
или отсутствует, mkdirSync()
возвращает undefined
. Если директория (или файл) уже существует или отсутствует родительская директория, выбрасывается исключение;recursive
имеет значение true
, mkdirSync()
возвращает путь первой созданной директории. Если директория (или файл) уже существует, ничего не происходит. Если отсутствует родительская директория, она создается.Пример использования mkdirSync()
:
import * as fs from "node:fs";
assert.deepEqual(
Array.from(traverseDir(".")),
[
"dir",
]
);
fs.mkdirSync("dir/sub/subsub", { recursive: true });
assert.deepEqual(
Array.from(traverseDir(".")),
[
"dir",
"dir/sub",
"dir/sub/subsub",
]
);
При создании вложенных директорий и файлов мы не всегда может быть уверены в существовании родительской директории. Следующая функция может в этом помочь:
import * as path from "node:path";
import * as fs from "node:fs";
function ensureParentDirectory(filePath) {
const parentDir = path.dirname(filePath);
if (!fs.existsSync(parentDir)) {
fs.mkdirSync(parentDir, { recursive: true });
}
}
Пример использования:
assert.deepEqual(
Array.from(traverseDir(".")),
[
"dir",
]
);
const filePath = "dir/sub/subsub/new-file.txt";
ensureParentDirectory(filePath);
fs.writeFileSync(filePath, "content", { encoding: "utf-8" });
assert.deepEqual(
Array.from(traverseDir(".")),
[
"dir",
"dir/sub",
"dir/sub/subsub",
"dir/sub/subsub/new-file.txt",
]
);
fs.mkdtemp(pathPrefix, options?) создает временную директорию: она добавляет 6 произвольных символов к pathPrefix
, создает директорию и возвращает путь.
Обратите внимание: pathPrefix
не должен оканчиваться на заглавную X
, поскольку некоторые платформы заменяют X
произвольными символами.
Для создания временной директории внутри специфичной для операционной системы глобальной временной директории можно использовать функцию os.tmpdir:
import * as os from "node:os";
import * as path from "node:path";
import * as fs from "node:fs";
const pathPrefix = path.resolve(os.tmpdir(), "my-app");
// например, "/var/folders/ph/sz0384m11vxf/T/my-app"
const tmpPath = fs.mkdtempSync(pathPrefix);
// например, "/var/folders/ph/sz0384m11vxf/T/my-app1QXOXP"
Созданные таким способом директории автоматически не удаляются.
fs.cpSync(srcPath, destPath, options?) копирует файл или директорию из srcPath
в destPath
. Полезные настройки:
recursive
(false
по умолчанию): директории (включая пустые) копируются только когда данная настройка имеет значение true
;force
(true
): если имеет значение true
, существующие файлы перезаписываются:false
и настройка errorOnExist
установлена в true
, при наличии файла выбрасывается исключение;filter
: функция, позволяющая управлять тем, какие файлы копируются;preserveTimestamps
(false
): если имеет значение true
, копии в destPath
получат отметки времени оригиналов (время создания, последней модификации и т.п.).Пример использования:
import * as fs from "node:fs";
assert.deepEqual(
Array.from(traverseDir(".")),
[
"dir-orig",
"dir-orig/some-file.txt",
]
);
fs.cpSync("dir-orig", "dir-copy", { recursive: true });
assert.deepEqual(
Array.from(traverseDir(".")),
[
"dir-copy",
"dir-copy/some-file.txt",
"dir-orig",
"dir-orig/some-file.txt",
]
);
fs.renameSync(oldPath, newPath) переименовывает или перемещает файл или директорию из oldPath
в newPath
.
Пример использования данной функции для переименования директории:
import * as fs from "node:fs";
assert.deepEqual(
Array.from(traverseDir(".")),
[
"old-dir-name",
"old-dir-name/some-file.txt",
]
);
fs.renameSync("old-dir-name", "new-dir-name");
assert.deepEqual(
Array.from(traverseDir(".")),
[
"new-dir-name",
"new-dir-name/some-file.txt",
]
);
Пример перемещения файла:
assert.deepEqual(
Array.from(traverseDir(".")),
[
"dir",
"dir/subdir",
"dir/subdir/some-file.txt",
]
);
fs.renameSync("dir/subdir/some-file.txt", "some-file.txt");
assert.deepEqual(
Array.from(traverseDir(".")),
[
"dir",
"dir/subdir",
"some-file.txt",
]
);
rm
, rm -r
)fs.rmSync(path, options?) удаляет файл или директорию по указанному пути. Полезные настройки:
recursive
(false
): директории (включая пустые) удаляются только когда данная настройка имеет значение true
;force
(false
): если имеет значение false
, при отсутствии файла или директории по указанному пути выбрасывается исключение.Пример удаления файла:
import * as fs from "node:fs";
assert.deepEqual(
Array.from(traverseDir(".")),
[
"dir",
"dir/some-file.txt",
]
);
fs.rmSync("dir/some-file.txt");
assert.deepEqual(
Array.from(traverseDir(".")),
[
"dir",
]
);
Пример рекурсивного удаления непустой директории:
import * as fs from "node:fs";
assert.deepEqual(
Array.from(traverseDir(".")),
[
"dir",
"dir/subdir",
"dir/subdir/some-file.txt",
]
);
fs.rmSync("dir/subdir", {recursive: true});
assert.deepEqual(
Array.from(traverseDir(".")),
[
"dir",
]
);
rmdir
)fs.rmdirSync удаляет пустую директорию (если директория не является пустой, выбрасывается исключение):
import * as fs from "node:fs";
assert.deepEqual(
Array.from(traverseDir(".")),
[
"dir",
"dir/subdir",
]
);
fs.rmdirSync("dir/subdir");
assert.deepEqual(
Array.from(traverseDir(".")),
[
"dir",
]
);
Следующая функция очищает директорию по указанному пути:
import * as path from "node:path";
import * as fs from "node:fs";
function clearDir(dirPath) {
for (const fileName of fs.readdirSync(dirPath)) {
const pathName = path.join(dirPath, fileName);
fs.rmSync(pathName, { recursive: true });
}
}
Здесь:
fs.readdirSync(path)
возвращает названия всех потомков директории по указанному пути;fs.rmSync(path, options?)
удаляет файлы и директории.Пример использования:
assert.deepEqual(
Array.from(traverseDir(".")),
[
"dir",
"dir/dir-file.txt",
"dir/subdir",
"dir/subdir/subdir-file.txt"
]
);
clearDirectory("dir");
assert.deepEqual(
Array.from(traverseDir(".")),
[
"dir",
]
);
Библиотека trash перемещает файлы или директории в корзину. Она работает на macOS
, Windows
и Linux
.
Пример использования:
import trash from "trash";
await trash(["*.png", "!rainbow.png"]);
trash()
принимает строку или массив строк в качестве первого параметра. Любая строка может быть паттерном поиска (glob pattern) (со звездочками и другими метасимволами).
fs.existsSync(path) возвращает true
, если файл или директория по указанному пути существует:
import * as fs from "node:fs";
assert.deepEqual(
Array.from(traverseDir(".")),
[
"dir",
"dir/some-file.txt",
]
);
assert.equal(
fs.existsSync("dir"), true
);
assert.equal(
fs.existsSync("dir/some-file.txt"), true
);
assert.equal(
fs.existsSync("dir/non-existent-file.txt"), false
);
fs.statSync(path, options?) возвращает экземпляр fs.Stats с полезной информацией о файле или директории по указанному пути. Основные настройки:
throwIfNoEntry
(true
): если true
, при отсутствии записи выбрасывается исключение, если false
, возвращается undefined
;bigint
(false
): если true
, функция использует BigInt для числовых значений (таких как отметки времени, см. ниже).Свойства экземпляров fs.Stats
:
stats.isFile()
;stats.isDirectory()
;stats.isSymbolicLink()
;stats.size
: размер в байтах;stats.atime
: время последнего доступа;stats.mtime
: время последней модификации;stats.birthtime
: время создания;atime
:stats.atime
: экземпляр Date
;stats.atimeMS
: миллисекунды с начала эпохи (POSIX
);stats.atimeNs
: наносекунды с начала эпохи.Пример реализации функции isDirectory
с помощью fs.statsSync()
:
import * as fs from "node:fs";
function isDirectory(thePath) {
const stats = fs.statSync(thePath, { throwIfNoEntry: false });
return stats && stats.isDirectory();
}
assert.deepEqual(
Array.from(traverseDir(".")),
[
"dir",
"dir/some-file.txt",
]
);
assert.equal(
isDirectory("dir"), true
);
assert.equal(
isDirectory("dir/some-file.txt"), false
);
assert.equal(
isDirectory("non-existent-dir"), false
);
Функции для модификации атрибутов файла:
atime
: время последнего доступа;mtime
: время последней модификации.Функции для работы с жесткими ссылками (hard links):
Функции для работы с символическими ссылками (symbolic links):
path
на target
;Следующие функции оперируют символическими ссылками без их разыменования (dereferencing) (обратите внимание на префикс l
):
Еще одна полезная функция — fs.realpathSync(path, options?) вычисляет каноническое название пути посредством разрешения символов .
и ..
, а также символических ссылок.
Настройки функций, влияющие на обработку символических ссылок:
fs.cpSync(srcPath, destPath, options?)
:dereference
(false
): если true
, копируется файл, на который указывает символическая ссылка, а не сама ссылка;verbatimSymlinks
(false
): если false
, обновляется указатель локации цели скопированной символической ссылки.Ссылки для дальнейшего изучения материала.
Надеюсь, вы, как и я, узнали что-то новое и не зря потратили время.
Благодарю за внимание и happy coding!