[Перевод] Руководство по Node.js, часть 9: работа с файловой системой
Сегодня, в девятой части перевода руководства по Node.js, мы поговорим о работе с файлами. В частности, речь пойдёт о модулях fs и path — о файловых дескрипторах, о путях к файлам, о получении информации о файлах, об их чтении и записи, о работе с директориями.
Часть 2: JavaScript, V8, некоторые приёмы разработки
Часть 3: Хостинг, REPL, работа с консолью, модули
Часть 4: npm, файлы package.json и package-lock.json
Часть 5: npm и npx
Часть 6: цикл событий, стек вызовов, таймеры
Часть 7: асинхронное программирование
Часть 8: Руководство по Node.js, часть 8: протоколы HTTP и WebSocket
Часть 9: Руководство по Node.js, часть 9: работа с файловой системой
Работа с файловыми дескрипторами в Node.js
Прежде чем вы сможете взаимодействовать с файлами, находящимися в файловой системе вашего сервера, вам необходимо получить дескриптор файла.
Дескриптор можно получить, воспользовавшись для открытия файла асинхронным методом open()
из модуля fs
:
const fs = require('fs')
fs.open('/Users/flavio/test.txt', 'r', (err, fd) => {
//fd - это дескриптор файла
})
Обратите внимание на второй параметр, r
, использованный при вызове метода fs.open()
. Это — флаг, который сообщает системе о том, что файл открывают для чтения. Вот ещё некоторые флаги, которые часто используются при работе с этим и некоторыми другими методами:
r+
— открыть файл для чтения и для записи.w+
— открыть файл для чтения и для записи, установив указатель потока в начало файла. Если файл не существует — он создаётся.a
— открыть файл для записи, установив указатель потока в конец файла. Если файл не существует — он создаётся.a+
— открыть файл для чтения и записи, установив указатель потока в конец файла. Если файл не существует — он создаётся.
Файлы можно открывать и пользуясь синхронным методом 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
. Что это за информация? На самом деле, соответствующий объект предоставляет нам большое количество полезных свойств и методов:
- Методы
.isFile()
и.isDirectory()
позволяют, соответственно, узнать, является ли исследуемый файл обычным файлом или директорией. - Метод
.isSymbolicLink()
позволяет узнать, является ли файл символической ссылкой. - Размер файла можно узнать, воспользовавшись свойством
.size
.
Тут имеются и другие методы, но эти — самые употребимые. Вот как ими пользоваться:
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
})
Пути к файлам в Node.js и модуль path
Путь к файлу — это адрес того места в файловой системе, где он расположен.
В Linux и macOS путь может выглядеть так:
/users/flavio/file.txt
В Windows пути выглядят немного иначе:
C:\users\flavio\file.txt
На различия в форматах записи путей при использовании разных операционных систем следует обращать внимание, учитывая операционную систему, используемую для развёртывания Node.js-сервера.
В Node.js есть стандартный модуль path
, предназначенный для работы с путями к файлам. Перед использованием этого модуля в программе его надо подключить:
const path = require('path')
▍Получение информации о пути к файлу
Если у вас есть путь к файлу, то, используя возможности модуля path
, вы можете, в удобном для восприятия и дальнейшей обработки виде, узнать подробности об этом пути. Выглядит это так:
const notes = '/users/flavio/notes.txt'
path.dirname(notes) // /users/flavio
path.basename(notes) // notes.txt
path.extname(notes) // .txt
Здесь, в строке notes
, хранится путь к файлу. Для разбора пути использованы следующие методы модуля path
:
dirname()
— возвращает родительскую директорию файла.basename()
— возвращает имя файла.extname()
— возвращает расширение файла.
Узнать имя файла без расширения можно, вызвав метод .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
Самый простой способ чтения файлов в 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
В 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
}
//готово!
})
Об использовании потоков
Выше мы описывали методы, которые, выполняя запись в файл, пишут в него весь объём переданных им данных, после чего, если используются их синхронные версии, возвращают управление программе, а если применяются асинхронные версии — вызывают коллбэки. Если вас такое состояние дел не устраивает — лучше будет воспользоваться потоками.
Работа с директориями в Node.js
Модуль 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
Выше мы уже сталкивались с некоторыми методами модуля fs
, применяемыми при работе с файловой системой. На самом деле, он содержит ещё много полезного. Напомним, что он не нуждается в установке, для того, чтобы воспользоваться им в программе, его достаточно подключить:
const fs = require('fs')
После этого у вас будет доступ к его методам, среди которых отметим следующие, некоторые из которых вам уже знакомы:
fs.access()
: проверяет существование файла и возможность доступа к нему с учётом разрешений.fs.appendFile()
: присоединяет данные к файлу. Если файл не существует — он будет создан.fs.chmod()
: изменяет разрешения для заданного файла. Похожие методы:fs.lchmod()
,fs.fchmod()
.fs.chown()
: изменяет владельца и группу для заданного файла. Похожие методы:fs.fchown()
,fs.lchown()
.fs.close()
: закрывает дескриптор файла.fs.copyFile()
: копирует файл.fs.createReadStream()
: создаёт поток чтения файла.fs.createWriteStream()
: создаёт поток записи файла.fs.link()
: создаёт новую жёсткую ссылку на файл.fs.mkdir()
: создаёт новую директорию.fs.mkdtemp()
: создаёт временную директорию.fs.open()
: открывает файл.fs.readdir()
: читает содержимое директории.fs.readFile()
: считывает содержимое файла. Похожий метод:fs.read()
.fs.readlink()
: считывает значение символической ссылки.fs.realpath()
: разрешает относительный путь к файлу, построенный с использованием символов.
и..
, в полный путь.fs.rename()
: переименовывает файл или папку.fs.rmdir()
: удаляет папку.fs.stat()
: возвращает сведения о файле. Похожие методы:fs.fstat()
,fs.lstat()
.fs.symlink()
: создаёт новую символическую ссылку на файл.fs.truncate()
: обрезает файл до заданной длины. Похожий метод:fs.ftruncate()
.fs.unlink()
: удаляет файл или символическую ссылку.fs.unwatchFile()
: отключает наблюдение за изменениями файла.fs.utimes()
: изменяет временную отметку файла. Похожий метод:fs.futimes()
.fs.watchFile()
: включает наблюдение за изменениями файла. Похожий метод:fs.watch()
.fs.writeFile()
: записывает данные в файл. Похожий метод:fs.write()
.
Интересной особенностью модуля fs
является тот факт, что все его методы, по умолчанию, являются асинхронными, но существуют и их синхронные версии, имена которых получаются путём добавления слова Sync
к именам асинхронных методов.
Например:
fs.rename()
fs.renameSync()
fs.write()
fs.writeSync()
Использование синхронных методов серьёзно влияет на то, как работает программа.
В 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
Модуль path, о некоторых возможностях которого мы тоже уже говорили, содержит множество полезных инструментов, позволяющих взаимодействовать с файловой системой. Как уже было сказано, устанавливать его не нужно, так как он является частью Node.js. Для того чтобы пользоваться им, его достаточно подключить:
const path = require('path')
Свойство path.sep
этого модуля предоставляет символ, использующийся для разделения сегментов пути (\
в Windows и /
в Linux и macOS), а свойство path.delimiter
даёт символ, используемый для отделения друг от друга нескольких путей (;
в Windows и :
в Linux и macOS).
Рассмотрим и проиллюстрируем примерами некоторые методы модуля path
.
▍path.basename ()
Возвращает последний фрагмент пути. Передав второй параметр этому методу можно убрать расширение файла.
require('path').basename('/test/something') //something
require('path').basename('/test/something.txt') //something.txt
require('path').basename('/test/something.txt', '.txt') //something
▍path.dirname ()
Возвращает ту часть пути, которая представляет имя директории:
require('path').dirname('/test/something') // /test
require('path').dirname('/test/something/file.txt') // /test/something
▍path.extname()
Возвращает ту часть пути, которая представляет расширение файла:
▍path.isAbsolute ()
Возвращает истинное значение если путь является абсолютным:
require('path').isAbsolute('/test/something') // true
require('path').isAbsolute('./test/something') // false
▍path.join ()
Соединяет несколько частей пути:
const name = 'flavio'
require('path').join('/', 'users', name, 'notes.txt') //'/users/flavio/notes.txt'
▍path.normalize ()
Пытается выяснить реальный путь на основе пути, который содержит символы, использующиеся при построении относительных путей вроде .
, ..
и //
:
require('path').normalize('/users/flavio/..//test.txt') ///users/test.txt
▍path.parse ()
Преобразует путь в объект, свойства которого представляют отдельные части пути:
root
: корневая директория.dir
: путь к файлу, начиная от корневой директорииbase
: имя файла и расширение.name
: имя файла.ext
: расширение файла.
Вот пример использования этого метода:
require('path').parse('/users/test.txt')
В результате его работы получается такой объект:
{
root: '/',
dir: '/users',
base: 'test.txt',
ext: '.txt',
name: 'test'
}
▍path.relative ()
Принимает, в качестве аргументов, 2 пути. Возвращает относительный путь из первого пути ко второму, основываясь на текущей рабочей директории:
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 ()
Находит абсолютный путь на основе переданного ему относительного пути:
path.resolve('flavio.txt')
//'/Users/flavio/flavio.txt' при запуске из моей домашней папки.
Итоги
Сегодня мы рассмотрели модули Node.js fs
и path
, которые используются для работы с файловой системой. В следующей части этой серии, на которой она завершается, мы обсудим модули os
, events
, http
, поговорим о работе с потоками и с системами управления базами данных в Node.js.
Уважаемые читатели! Какими npm-пакетами вы пользуетесь при работе с файловой системой в Node.js?