Сегодня, в девятой части перевода руководства по Node.js, мы поговорим о работе с файлами. В частности, речь пойдёт о модулях fs и path - о файловых дескрипторах, о путях к файлам, о получении информации о файлах, об их чтении и записи, о работе с директориями.
Дескриптор можно получить, воспользовавшись для открытия файла асинхронным методом open() из модуля fs:
Const fs = require("fs")
fs.open("/Users/flavio/test.txt", "r", (err, fd) => {
//fd - это дескриптор файла
})
Обратите внимание на второй параметр, r , использованный при вызове метода fs.open() . Это - флаг, который сообщает системе о том, что файл открывают для чтения. Вот ещё некоторые флаги, которые часто используются при работе с этим и некоторыми другими методами:
Const fs = require("fs")
try {
const fd = fs.openSync("/Users/flavio/test.txt", "r")
} catch (err) {
console.error(err)
}
После получения дескриптора любым из вышеописанных способов вы можете производить с ним необходимые операции.
Вызывают этот метод, передавая ему путь к файлу, и, после того, как Node.js получит необходимые сведения о файле, он вызовет коллбэк, переданный методу stat() . Вот как это выглядит:
Const fs = require("fs")
fs.stat("/Users/flavio/test.txt", (err, stats) => {
if (err) {
console.error(err)
return
}
//сведения о файле содержатся в аргументе `stats`
})
В Node.js имеется возможность синхронного получения сведений о файлах. При таком подходе главный поток блокируется до получения свойств файла:
Const fs = require("fs")
try {
const stats = fs.statSync ("/Users/flavio/test.txt")
} catch (err) {
console.error(err)
}
Информация о файле попадёт в константу stats . Что это за информация? На самом деле, соответствующий объект предоставляет нам большое количество полезных свойств и методов:
Const fs = require("fs") fs.stat("/Users/flavio/test.txt", (err, stats) => { if (err) { console.error(err) return } stats.isFile() //true stats.isDirectory() //false stats.isSymbolicLink() //false stats.size //1024000 //= 1MB })
В Linux и macOS путь может выглядеть так:
/users/flavio/file.txt
В Windows пути выглядят немного иначе:
C:\users\flavio\file.txt
На различия в форматах записи путей при использовании разных операционных систем следует обращать внимание, учитывая операционную систему, используемую для развёртывания Node.js-сервера.
В Node.js есть стандартный модуль path , предназначенный для работы с путями к файлам. Перед использованием этого модуля в программе его надо подключить:
Const notes = "/users/flavio/notes.txt"
path.dirname(notes) // /users/flavio
path.basename(notes) // notes.txt
path.extname(notes) // .txt
Здесь, в строке notes , хранится путь к файлу. Для разбора пути использованы следующие методы модуля path:
Path.basename(notes, path.extname(notes)) //notes
Const name = "flavio"
path.join("/", "users", name, "notes.txt") //"/users/flavio/notes.txt"
Найти абсолютный путь к файлу на основе относительного пути к нему можно с использованием метода path.resolve() :
Path.resolve("flavio.txt")
//"/Users/flavio/flavio.txt" при запуске из моей домашней папки
В данном случае Node.js просто добавляет /flavio.txt к пути, ведущем к текущей рабочей директории. Если при вызове этого метода передать ещё один параметр, представляющий путь к папке, метод использует его в качестве базы для определения абсолютного пути:
Path.resolve("tmp", "flavio.txt")
// "/Users/flavio/tmp/flavio.txt" при запуске из моей домашней папки
Если путь, переданный в качестве первого параметра, начинается с косой черты - это означает, что он представляет собой абсолютный путь.
Path.resolve("/etc", "flavio.txt")
// "/etc/flavio.txt"
Вот ещё один полезный метод - path.normalize() . Он позволяет найти реальный путь к файлу, используя путь, в котором содержатся спецификаторы относительного пути вроде точки (.), двух точек (..), или двух косых черт:
Path.normalize("/users/flavio/..//test.txt")
// /users/test.txt
Методы resolve() и normalize() не проверяют существование директории. Они просто находят путь, основываясь на переданным им данным.
Fs.readFile("/Users/flavio/test.txt", (err, data) => {
if (err) {
console.error(err)
return
}
console.log(data)
})
Если надо, можно воспользоваться синхронной версией этого метода - fs.readFileSync() :
Const fs = require("fs")
try {
const data = fs.readFileSync("/Users/flavio/test.txt")
console.log(data)
} catch (err) {
console.error(err)
}
По умолчанию при чтении файлов используется кодировка utf8 , но кодировку можно задать и самостоятельно, передав методу соответствующий параметр.
Методы fs.readFile() и fs.readFileSync() считывают в память всё содержимое файла. Это означает, что работа с большими файлами с применением этих методов серьёзно отразится на потреблении памяти вашим приложением и окажет влияние на его производительность. Если с такими файлами нужно работать, лучше всего воспользоваться потоками.
Const fs = require("fs")
const content = "Some content!"
fs.writeFile("/Users/flavio/test.txt", content, (err) => {
if (err) {
console.error(err)
return
}
//файл записан успешно
})
Есть и синхронная версия того же метода - fs.writeFileSync() :
Const fs = require("fs")
const content = "Some content!"
try {
const data = fs.writeFileSync("/Users/flavio/test.txt", content)
//файл записан успешно
} catch (err) {
console.error(err)
}
Эти методы, по умолчанию, заменяют содержимое существующих файлов. Изменить их стандартное поведение можно, воспользовавшись соответствующим флагом:
Fs.writeFile("/Users/flavio/test.txt", content, { flag: "a+" }, (err) => {})
Тут могут использоваться флаги, которые мы уже перечисляли в разделе, посвящённом дескрипторам. Подробности о флагах можно узнать .
Const content = "Some content!" fs.appendFile("file.log", content, (err) => { if (err) { console.error(err) return } //готово! })
Const fs = require("fs") const folderName = "/Users/flavio/test" try { if (!fs.existsSync(dir)){ fs.mkdirSync(dir) } } catch (err) { console.error(err) }
Const fs = require("fs")
const path = require("path")
const folderPath = "/Users/flavio"
fs.readdirSync(folderPath)
Вот так можно получить полный путь к файлу:
Fs.readdirSync(folderPath).map(fileName => {
return path.join(folderPath, fileName)
}
Результаты можно отфильтровать для того, чтобы получить только файлы и исключить из вывода директории:
Const isFile = fileName => { return fs.lstatSync(fileName).isFile() } fs.readdirSync(folderPath).map(fileName => { return path.join(folderPath, fileName)).filter(isFile) }
Const fs = require("fs")
fs.rename("/Users/flavio", "/Users/roger", (err) => {
if (err) {
console.error(err)
return
}
//готово
})
Переименовать папку можно и с помощью синхронного метода fs.renameSync() :
Const fs = require("fs") try { fs.renameSync("/Users/flavio", "/Users/roger") } catch (err) { console.error(err) }
Метод remove() из пакета fs-extra умеет удалять папки, в которых уже что-то есть.
Установить этот модуль можно так:
Npm install fs-extra
Вот пример его использования:
Const fs = require("fs-extra")
const folder = "/Users/flavio"
fs.remove(folder, err => {
console.error(err)
})
Его методами можно пользоваться в виде промисов:
Fs.remove(folder).then(() => {
//готово
}).catch(err => {
console.error(err)
})
Допустимо и применение конструкции async/await:
Async function removeFolder(folder) { try { await fs.remove(folder) //готово } catch (err) { console.error(err) } } const folder = "/Users/flavio" removeFolder(folder)
Const fs = require("fs")
После этого у вас будет доступ к его методам, среди которых отметим следующие, некоторые из которых вам уже знакомы:
Например:
В Node.js 10 имеется экспериментальная поддержка этих API , основанных на промисах.
Исследуем метод fs.rename() . Вот асинхронная версия этого метода, использующая коллбэки:
Const fs = require("fs")
fs.rename("before.json", "after.json", (err) => {
if (err) {
return console.error(err)
}
//готово
})
При использовании его синхронной версии для обработки ошибок используется конструкция try/catch:
Const fs = require("fs")
try {
fs.renameSync("before.json", "after.json")
//готово
} catch (err) {
console.error(err)
}
Основное различие между этими вариантами использования данного метода заключается в том, что во втором случае выполнение скрипта будет заблокировано до завершения файловой операции.
Const path = require("path")
Свойство path.sep этого модуля предоставляет символ, использующийся для разделения сегментов пути (\ в Windows и / в Linux и macOS), а свойство path.delimiter даёт символ, используемый для отделения друг от друга нескольких путей (; в Windows и: в Linux и macOS).
Рассмотрим и проиллюстрируем примерами некоторые методы модуля path .
Require("path").basename("/test/something") //something require("path").basename("/test/something.txt") //something.txt require("path").basename("/test/something.txt", ".txt") //something
Require("path").dirname("/test/something") // /test require("path").dirname("/test/something/file.txt") // /test/something
Require("path").extname("/test/something") // "" require("path").extname("/test/something/file.txt") // ".txt"
Require("path").isAbsolute("/test/something") // true require("path").isAbsolute("./test/something") // false
Const name = "flavio" require("path").join("/", "users", name, "notes.txt") //"/users/flavio/notes.txt"
Require("path").normalize("/users/flavio/..//test.txt") ///users/test.txt
Require("path").parse("/users/test.txt")
В результате его работы получается такой объект:
{ root: "/", dir: "/users", base: "test.txt", ext: ".txt", name: "test" }
Require("path").relative("/Users/flavio", "/Users/flavio/test.txt") //"test.txt" require("path").relative("/Users/flavio", "/Users/flavio/something/test.txt") //"something/test.txt"
Path.resolve("flavio.txt") //"/Users/flavio/flavio.txt" при запуске из моей домашней папки.
Уважаемые читатели! Какими npm-пакетами вы пользуетесь при работе с файловой системой в Node.js?
Операции с файлами языку JavaScript не в новинку — в JScript, встроенном в Windows, доступен полный набор функций для работы с диском. Node, в силу своей асинхронной природы, несколько усложняет эти в общем то тривиальные задачи.
Сразу хочу предупредить об одной возможной ошибке. Если Вы, как и я, запускаете Node в виртуальной машине из общей папки, помните — VM в эту папку писать не может. Попытки создать или дополнить файлы в ней закончатся только Error: Permission denied
Обращение к файлам на диске — операция небыстрая. Она в среднем в десятки тысяч раз дольше чем обращение к оперативной памяти. Поэтому, большинство операций с файлами асинхронные. Все операции с файловой системой собраны во встроенном модуле fs , стало быть начнём с его подключения.
Var fs = require("fs"), sys = require("sys");
Модуль sys нам нужен для вывода информации в консоль. В последующих примерах я эти строки буду опускать, чтобы не повторяться.
Открытие файла делается так:
Fs.open(<путь> , <флаги> , <режим доступа> , <функция-обработчик> )
Например:
Fs.open("readme.txt", "r+", 0644, function(err, file_handle) { if (!err) { // Операции с открытым файлом } else { // Обработка ошибок } });
Для записи в файл используется метод fs.write:
Fs.write(<дескриптор> , <данные> , <позиция> , <кодировка> , <обработчик> )
Расширим предыдущий пример записью строки 🙂
Fs.open("readme.txt", "a", 0644, function(err, file_handle) { if (!err) { // Записываем в конец файла readme.txt фразу "Copyrighted by Me" // при открытии в режиме "a" указатель уже в конце файла, и мы передаём null // в качестве позиции fs.write(file_handle, "Copyrighted by Me", null, "ascii", function(err, written) { if (!err) { // Всё прошло хорошо } else { // Произошла ошибка при записи } }); } else { // Обработка ошибок при открытии } });
Чтение делается так:
Fs.read(<дескриптор> , <длина> , <позиция> , <кодировка> , <обработчик> )
Здесь всё почти так же, как в fs.write .
Чтение из файла — совсем несложный процесс:
Fs.open("readme.txt", "r", 0644, function(err, file_handle) { if (!err) { // Читаем 10 килобайт с начала файла, в ascii fs.read(file_handle, 10000, null, "ascii", function(err, data) { if (!err) { // Всё прошло хорошо, выводим прочитанное в консоль sys.puts(data); } else { // Произошла ошибка при чтении } }); } else { // Обработка ошибок при открытии файла } });
После того как чтение/запись завершены, файл надо закрыть. Node может обслуживать одновременно очень много клиентов, поэтому ресурсы лучше освобождать сразу когда они становятся не нужны.
Fs.open("readme.txt", "r", 0644, function(err, file_handle) { if (!err) { // Читаем 10 килобайт с начала файла, в ascii fs.read(file_handle, 10000, null, "ascii", function(err, data) { if (!err) { // Всё прошло хорошо, выводим прочитанное в консоль sys.puts(data); fs.close(file_handle); } else { // Произошла ошибка при чтении } }); } else { // Обработка ошибок при открытии файла } });
Вторым аргументом fs.close может принимать функцию-callback, которой передаётся исключение в случае ошибки.
У всех перечисленных функций есть синхронные варианты. К их названию добавлено Sync и они не принимают последним аргументом функцию-обработчик, а просто возвращают соответствующее значение (или бросают исключение). Обратите внимание, readSync возвращает массив из данных и количества прочитанных байт.
Var file_handle = fs.openSync("readme.txt", "r", 0644); var data = fs.readSync(file_handle, 10000, null, "ascii"); sys.puts(data); fs.closeSync(file_handle);
Был рассмотрен переход от синхронного кода к асинхронному. Во второй части я рассмотрю переход от побайтового чтения из файла к построчному. Поскольку операция кодирования из одной кодировки в другую довольно тривиальна, я чуть больше расскажу про теоретическую сторону вопроса.
Fs.readFile("large.txt", { encoding: "utf8" }, (err, data) => { if (err) throw err; data.split("\n").forEach(line => { doSomethingWithLine(line); }); });
Он же, пожалуй, самый быстрый. Но он же требует больше всего памяти — от 100% до 200% от размера файла. 200% — это одновременно и самый распространенный случай, так как в памяти кодировка у строки UTF-16 и поэтому размер требуемой памяти умножается на два если файл содержит в основном символы из однобайтного диапазона UTF-8.
Кроме того, разработчики Node.js не рекомендуют загружать много данных в Node.js процесс (см. What is the memory limit on a node process?) . Сделано это не очень элегантно — даже если физической памяти хватает, то при попытке загрузить файл больше 1Gb бросается исключение:
This.parent = new SlowBuffer(this.length); ^ RangeError: length > kMaxLength
Если же файл поменьше, то можно получить и такое:
FATAL ERROR: CALL_AND_RETRY_0 Allocation failed - process out of memory
Остается только обрабатывать файл по частям. Для этого нужно его по частям прочитать и Node.js предоставляет для этого минимум 5 способов:
Варианты 1-3 являются более элегантными, так как оперируют удобной абстракцией — потоком. Это позволяет рассматривать программу как диаграмму потоков данных (data flow diagram) и при дизайне архитектуры оперировать такими терминами как слияние, разделение и трансформация.
Также варианты 1 и 2 отличаются возможностью чтения символов из файла. В вариантах 3 и 4 данные из файла записываются в буфер и затем их надо конвертировать в текст.
// Вариант #1 - "старые" потоки var stream = fs.createReadStream(file, { encoding: "utf8" }); stream.on("data", (_, data) => processData(data)); stream.on("end", done); // Вариант #2 - "новые" потоки var stream = fs.createReadStream(file, { encoding: "utf8" }); stream.on("readable", () => processData(stream.read())); stream.on("end", done); // Вариант #3 - pipe var stream = fs.createReadStream(file, { encoding: "utf8" }); var writeStream = new Writable(); writeStream._write = (chunk, encoding, callback) => { processData(chunk); callback(); }; writeStream.on("end", done); stream.pipe(writeStream); // Вариант #4 - асинхронные методы fs fs.open(file, "r", (err, fd) => { var buffer = new Buffer(1000*1000); (function next() { fs.read(fd, buffer, 0, buffer.length, null, (err, bytesRead, buffer) => { if (bytesRead === 0) { fs.close(fd, done); } else { processData(buffer); next(); } }); }()); });
Более концептуальным является отличие с точки зрения получения данных из файла. Варианты 1-2 получают следующий фрагмент как только завершается обработчик события текущего фрагмента. В случае асинхронного кода в обработчике последовательность его выполнения непредсказуема:
Function processData(chunk) { console.log("first") setImmediate(() => { console.log("second"); setImmediate(() => console.log("third")); }); } var stream = fs.createReadStream(file, { encoding: "utf8" }); stream.on("readable", () => processData(stream.read())); ... first third second third first second ...
Ситуацию можно поправить используя методы pause()/resume().
Function processData(chunk, done) { console.log("first") setImmediate(() => { console.log("second"); setImmediate(() => { console.log("third"); done(); }); }); } var stream = fs.createReadStream(file, { encoding: "utf8" }); stream.on("readable", () => { stream.pause(); processData(stream.read(), () => stream.resume()); }); ... first second third first second third ...
В вариантах 3 и 4 следующий фрагмент будет получен только после передачи управления (вариант 3) или запроса (вариант 4).
Думаю, что информации достаточно для реализации функции createTextReader() из первой части статьи. Из всех вариантов наиболее соответствующим является четвертый, поскольку поток управления у него аналогичен интерфейсу (request-callback).
Function createTextReader(file, options, done) { var length, encoding, separator; if ("function" === typeof options) { done = options; options = { }; } length = 4 * 1024; encoding = options.encoding || "utf8"; separator = (options.separator || "\n"); fs.open(file, "r", (err, fd) => { var eof, tail, buffer, decoder, lines; if (err) { done(err); return; } eof = false; buffer = new Buffer(length); tail = ""; lines = ; decoder = new StringDecoder(encoding); done(null, { readLine: done => { var line; if (lines.length > 0) { line = lines.shift(); done(null, line); } else if (eof) { done(null, null); } else { (function read() { fs.read(fd, buffer, 0, length, null, function (err, bytesRead, buffer) { var index, position; if (bytesRead === 0) { eof = true; done(null, tail); } else { tail = tail + decoder.write(buffer.slice(0, bytesRead)); index = -1; while (-1 !== (position = tail.indexOf(separator, index))) { lines.push(tail.substring(index, position)); index = position + separator.length; } tail = tail.substring(index); if (lines.length === 0) { read(); } else { line = lines.shift(); done(null, line); } } }); }()); } }, close: done => { fs.close(fd, () => { if (done) { done(err || null); } }); } }); }); }
В двух частях этой статьи я постарался изложить все, что мне пригодилось при создании модуля https://github.com/AlexAtNet/async-read-lines . К сожалению, многое осталось за рамками, не на все хватило времени. Так что если нашли ошибку или опечатку — пишите в личные сообщения. Если у вас есть вопросы по теме статьи — буду рад ответить в комментариях. Если увидите баги в модуле — создавайте запрос в github issues . Связаться со мной лично можно через сайт alexatnet.com .
Успехов в программировании!
Об авторе: Александр Неткачев — старший разработчик на С# и F#. Поддерживает сайт alexatnet.com , проводит вебинары (Code&Coffe), помогает с кодом начинающим разработчикам (CodeReview4U).
Всем привет! В этой статье мы рассмотрим, как записывать и читать файлы в NodeJS .
Платформа NodeJS позволяет записывать и читать файлы в операционной системе. Для этого нам потребуется использовать модуль FS (file system ).
Var fs = require("fs");
Для демонстрации считывания содержимого файлов давайте создадим файлик с названием readme.txt .
// содержимое файла readme.txt
Здесь какое-нибудь содержимое файла
Var text = fs.readFileSync("readme.txt", "utf8");
console.log(text);
Мы используем метод, в который передаем первым параметром имя файла, а вторым – кодировку. Как понятно из названия, этот метод является синхронным. Это значит, что весь код, который идет ниже, выполнится только тогда, когда весь файл будет прочитан. Дальше мы просто записываем полученные данные в переменную, которую потом выводим на экран.
Теперь давайте попробуем считанное нами содержание файла записать в новый файл. Для этого напишем следующее:
Fs.writeFileSync("writeme.txt", text);
Теперь после запуска кода вы увидите, что создался новый файлик с названием writeme.txt , в котором будет содержимое, записанное в переменную text из файла readme.txt .
Давайте рассмотрим, как использовать методы асинхронно. Например, считаем файлик readme.txt :
console.log(data);
});
Console.log("выведется раньше, чем данные из файла");
Использование почти такое же, но теперь мы также третьим параметром передаем функцию, где в качестве аргументов первым идет ошибка, а вторым содержимое файла, которое мы потом и выводим. Ниже я написал еще один вывод текста, чтобы показать вам, что метод действительно асинхронный, поэтому, пока идет считывание файла, выполнится код ниже, а только потом выведется текст из файлика.
Теперь давайте снова считаем содержимое файла readme.txt и запишем его в файл writeme.txt , но только теперь асинхронно.
Fs.readFile("readme.txt", "utf8", function(error, data) {
fs.writeFile("writeme.txt", data);
});
А на этом у меня сегодня все. Спасибо за внимание!
Сегодня, в девятой части перевода руководства по Node.js, мы поговорим о работе с файлами. В частности, речь пойдёт о модулях fs и path - о файловых дескрипторах, о путях к файлам, о получении информации о файлах, об их чтении и записи, о работе с директориями.
Прежде чем вы сможете взаимодействовать с файлами, находящимися в файловой системе вашего сервера, вам необходимо получить дескриптор файла.
Дескриптор можно получить, воспользовавшись для открытия файла асинхронным методом open() из модуля fs:
Const fs = require("fs") fs.open("/Users/flavio/test.txt", "r", (err, fd) => { //fd - это дескриптор файла })
Обратите внимание на второй параметр, r , использованный при вызове метода fs.open() . Это - флаг, который сообщает системе о том, что файл открывают для чтения. Вот ещё некоторые флаги, которые часто используются при работе с этим и некоторыми другими методами:
Файлы можно открывать и пользуясь синхронным методом fs.openSync() , который, вместо того, чтобы предоставить дескриптор файла в коллбэке, возвращает его:
Const fs = require("fs") try { const fd = fs.openSync("/Users/flavio/test.txt", "r") } catch (err) { console.error(err) }
После получения дескриптора любым из вышеописанных способов вы можете производить с ним необходимые операции.
С каждым файлом связан набор данных о нём, исследовать эти данные можно средствами Node.js. В частности, сделать это можно, используя метод stat() из модуля fs .
Вызывают этот метод, передавая ему путь к файлу, и, после того, как Node.js получит необходимые сведения о файле, он вызовет коллбэк, переданный методу stat() . Вот как это выглядит:
Const fs = require("fs") fs.stat("/Users/flavio/test.txt", (err, stats) => { if (err) { console.error(err) return } //сведения о файле содержатся в аргументе `stats` })
В Node.js имеется возможность синхронного получения сведений о файлах. При таком подходе главный поток блокируется до получения свойств файла:
Const fs = require("fs") try { const stats = fs.statSync ("/Users/flavio/test.txt") } catch (err) { console.error(err) }
Информация о файле попадёт в константу stats . Что это за информация? На самом деле, соответствующий объект предоставляет нам большое количество полезных свойств и методов:
Тут имеются и другие методы, но эти - самые употребимые. Вот как ими пользоваться:
Const fs = require("fs") fs.stat("/Users/flavio/test.txt", (err, stats) => { if (err) { console.error(err) return } stats.isFile() //true stats.isDirectory() //false stats.isSymbolicLink() //false stats.size //1024000 //= 1MB })
Путь к файлу - это адрес того места в файловой системе, где он расположен.
В Linux и macOS путь может выглядеть так:
/users/flavio/file.txt
В Windows пути выглядят немного иначе:
C:usersflaviofile.txt
На различия в форматах записи путей при использовании разных операционных систем следует обращать внимание, учитывая операционную систему, используемую для развёртывания Node.js-сервера.
В Node.js есть стандартный модуль path , предназначенный для работы с путями к файлам. Перед использованием этого модуля в программе его надо подключить:
Если у вас есть путь к файлу, то, используя возможности модуля path , вы можете, в удобном для восприятия и дальнейшей обработки виде, узнать подробности об этом пути. Выглядит это так:
Const notes = "/users/flavio/notes.txt" path.dirname(notes) // /users/flavio path.basename(notes) // notes.txt path.extname(notes) // .txt
Здесь, в строке notes , хранится путь к файлу. Для разбора пути использованы следующие методы модуля path:
Узнать имя файла без расширения можно, вызвав метод.basename() и передав ему второй аргумент, представляющий расширение:
Path.basename(notes, path.extname(notes)) //notes
Несколько частей пути можно объединить, используя метод path.join() :
Const name = "flavio" path.join("/", "users", name, "notes.txt") //"/users/flavio/notes.txt"
Найти абсолютный путь к файлу на основе относительного пути к нему можно с использованием метода path.resolve() :
Path.resolve("flavio.txt") //"/Users/flavio/flavio.txt" при запуске из моей домашней папки
В данном случае Node.js просто добавляет /flavio.txt к пути, ведущем к текущей рабочей директории. Если при вызове этого метода передать ещё один параметр, представляющий путь к папке, метод использует его в качестве базы для определения абсолютного пути:
Path.resolve("tmp", "flavio.txt") // "/Users/flavio/tmp/flavio.txt" при запуске из моей домашней папки
Если путь, переданный в качестве первого параметра, начинается с косой черты - это означает, что он представляет собой абсолютный путь.
Path.resolve("/etc", "flavio.txt") // "/etc/flavio.txt"
Вот ещё один полезный метод - path.normalize() . Он позволяет найти реальный путь к файлу, используя путь, в котором содержатся спецификаторы относительного пути вроде точки (.), двух точек (..), или двух косых черт:
Path.normalize("/users/flavio/..//test.txt") // /users/test.txt
Методы resolve() и normalize() не проверяют существование директории. Они просто находят путь, основываясь на переданным им данным.
Самый простой способ чтения файлов в Node.js заключается в использовании метода fs.readFile() с передачей ему пути к файлу и коллбэка, который будет вызван с передачей ему данных файла (или объекта ошибки):
Fs.readFile("/Users/flavio/test.txt", (err, data) => { if (err) { console.error(err) return } console.log(data) })
Если надо, можно воспользоваться синхронной версией этого метода - fs.readFileSync() :
Const fs = require("fs") try { const data = fs.readFileSync("/Users/flavio/test.txt") console.log(data) } catch (err) { console.error(err) }
По умолчанию при чтении файлов используется кодировка utf8 , но кодировку можно задать и самостоятельно, передав методу соответствующий параметр.
Методы fs.readFile() и fs.readFileSync() считывают в память всё содержимое файла. Это означает, что работа с большими файлами с применением этих методов серьёзно отразится на потреблении памяти вашим приложением и окажет влияние на его производительность. Если с такими файлами нужно работать, лучше всего воспользоваться потоками.
В Node.js легче всего записывать файлы с использованием метода fs.writeFile() :
Const fs = require("fs") const content = "Some content!" fs.writeFile("/Users/flavio/test.txt", content, (err) => { if (err) { console.error(err) return } //файл записан успешно })
Есть и синхронная версия того же метода - fs.writeFileSync() :
Const fs = require("fs") const content = "Some content!" try { const data = fs.writeFileSync("/Users/flavio/test.txt", content) //файл записан успешно } catch (err) { console.error(err) }
Эти методы, по умолчанию, заменяют содержимое существующих файлов. Изменить их стандартное поведение можно, воспользовавшись соответствующим флагом:
Fs.writeFile("/Users/flavio/test.txt", content, { flag: "a+" }, (err) => {})
Тут могут использоваться флаги, которые мы уже перечисляли в разделе, посвящённом дескрипторам. Подробности о флагах можно узнать .
Метод fs.appendFile() (и его синхронную версию - fs.appendFileSync()) удобно использовать для присоединения данных к концу файла:
Const content = "Some content!" fs.appendFile("file.log", content, (err) => { if (err) { console.error(err) return } //готово! })
Выше мы описывали методы, которые, выполняя запись в файл, пишут в него весь объём переданных им данных, после чего, если используются их синхронные версии, возвращают управление программе, а если применяются асинхронные версии - вызывают коллбэки. Если вас такое состояние дел не устраивает - лучше будет воспользоваться потоками.
Модуль fs предоставляет в распоряжение разработчика много удобных методов, которые можно использовать для работы с директориями.
Для того чтобы проверить, существует ли директория и может ли Node.js получить к ней доступ, учитывая разрешения, можно использовать метод fs.access() .
Для того чтобы создавать новые папки, можно воспользоваться методами fs.mkdir() и fs.mkdirSync() :
Const fs = require("fs") const folderName = "/Users/flavio/test" try { if (!fs.existsSync(dir)){ fs.mkdirSync(dir) } } catch (err) { console.error(err) }
Для того чтобы прочесть содержимое папки, можно воспользоваться методами fs.readdir() и fs.readdirSync() . В этом примере осуществляется чтение содержимого папки - то есть - сведений о том, какие файлы и поддиректории в ней имеются, и возврат их относительных путей:
Const fs = require("fs") const path = require("path") const folderPath = "/Users/flavio" fs.readdirSync(folderPath)
Вот так можно получить полный путь к файлу:
Fs.readdirSync(folderPath).map(fileName => { return path.join(folderPath, fileName) }
Результаты можно отфильтровать для того, чтобы получить только файлы и исключить из вывода директории:
Const isFile = fileName => { return fs.lstatSync(fileName).isFile() } fs.readdirSync(folderPath).map(fileName => { return path.join(folderPath, fileName)).filter(isFile) }
Для переименования папки можно воспользоваться методами fs.rename() и fs.renameSync() . Первый параметр - это текущий путь к папке, второй - новый:
Const fs = require("fs") fs.rename("/Users/flavio", "/Users/roger", (err) => { if (err) { console.error(err) return } //готово })
Переименовать папку можно и с помощью синхронного метода fs.renameSync() :
Const fs = require("fs") try { fs.renameSync("/Users/flavio", "/Users/roger") } catch (err) { console.error(err) }
Для того чтобы удалить папку, можно воспользоваться методами fs.rmdir() или fs.rmdirSync() . Надо отметить, что удаление папки, в которой что-то есть, задача несколько более сложная, чем удаление пустой папки. Если вам нужно удалять такие папки, воспользуйтесь пакетом fs-extra , который весьма популярен и хорошо поддерживается. Он представляет собой замену модуля fs , расширяющую его возможности.
Метод remove() из пакета fs-extra умеет удалять папки, в которых уже что-то есть.
Установить этот модуль можно так:
Npm install fs-extra
Вот пример его использования:
Const fs = require("fs-extra") const folder = "/Users/flavio" fs.remove(folder, err => { console.error(err) })
Его методами можно пользоваться в виде промисов:
Fs.remove(folder).then(() => { //готово }).catch(err => { console.error(err) })
Допустимо и применение конструкции async/await:
Async function removeFolder(folder) { try { await fs.remove(folder) //готово } catch (err) { console.error(err) } } const folder = "/Users/flavio" removeFolder(folder)
Выше мы уже сталкивались с некоторыми методами модуля fs , применяемыми при работе с файловой системой. На самом деле, он содержит ещё много полезного. Напомним, что он не нуждается в установке, для того, чтобы воспользоваться им в программе, его достаточно подключить:
Const fs = require("fs")
После этого у вас будет доступ к его методам, среди которых отметим следующие, некоторые из которых вам уже знакомы:
Интересной особенностью модуля fs является тот факт, что все его методы, по умолчанию, являются асинхронными, но существуют и их синхронные версии, имена которых получаются путём добавления слова Sync к именам асинхронных методов.
Например:
Использование синхронных методов серьёзно влияет на то, как работает программа.
В Node.js 10 имеется экспериментальная поддержка этих API , основанных на промисах.
Исследуем метод fs.rename() . Вот асинхронная версия этого метода, использующая коллбэки:
Const fs = require("fs") fs.rename("before.json", "after.json", (err) => { if (err) { return console.error(err) } //готово })
При использовании его синхронной версии для обработки ошибок используется конструкция try/catch:
Const fs = require("fs") try { fs.renameSync("before.json", "after.json") //готово } catch (err) { console.error(err) }
Основное различие между этими вариантами использования данного метода заключается в том, что во втором случае выполнение скрипта будет заблокировано до завершения файловой операции.
Модуль path, о некоторых возможностях которого мы тоже уже говорили, содержит множество полезных инструментов, позволяющих взаимодействовать с файловой системой. Как уже было сказано, устанавливать его не нужно, так как он является частью Node.js. Для того чтобы пользоваться им, его достаточно подключить:
Const path = require("path")
Свойство path.sep этого модуля предоставляет символ, использующийся для разделения сегментов пути (в Windows и / в Linux и macOS), а свойство path.delimiter даёт символ, используемый для отделения друг от друга нескольких путей (; в Windows и: в Linux и macOS).
Рассмотрим и проиллюстрируем примерами некоторые методы модуля path .
Возвращает последний фрагмент пути. Передав второй параметр этому методу можно убрать расширение файла.
Require("path").basename("/test/something") //something require("path").basename("/test/something.txt") //something.txt require("path").basename("/test/something.txt", ".txt") //something
Возвращает ту часть пути, которая представляет имя директории:
Require("path").dirname("/test/something") // /test require("path").dirname("/test/something/file.txt") // /test/something