Привет, друзья!
В этой небольшой статье я хочу рассказать вам о File System Access API
(далее — FSA
), позволяющем читать и записывать файлы в локальную систему пользователя с помощью браузера.
Основные источники:
Если вам это интересно, прошу под кат.
Поддержка
К сожалению, на сегодняшний день FSA
поддерживается только 34.68% браузеров: сюда входят все десктопные браузеры, за исключением Firefox
.
Возможности
FSA
расширяет объект window
следующими методами:
-
showOpenFilePicker
— для чтения файлов; -
showSaveFilePicker
— для записи файлов; -
showDirectoryPicker
— для чтения директории.
Данные методы называются фабриками дескрипторов локальной файловой системы (local file system handle factories) и возвращают FileSystemHandle
— сущность (entity) для работы с файлами (FileSystemFileHandle
) или директориями (FileSystemDirectoryHandle
), соответственно.
FileSystemHandle
содержит поле kind
(вид, тип), значением которого может быть либо file
, либо directory
, и поле name
(название файла или директории).
Чтение файла
Для получения сущности для чтения файла (FileSystemFileHandle
) используется метод showOpenFilePicker
:
const [fileHandle] = await window.showOpenFilePicker(options)
Общими для showOpenFilePicker
и showSaveFilePicker
являются настройки:
-
types?: object
— разрешенные типы файлов; -
excludeAcceptAllOption?: boolean
— если имеет значениеtrue
,picker
будет принимать/сохранять только файлы с типами, определенными вtypes
.
Значением поля types
является объект со следующими свойствами:
-
description?: string
— описание типа файлов; -
accept?: object
— объект вида{ MIME-тип: расширение }
.
Специфичной для showOpenFilePicker
настройкой является multiple?: boolean
— если имеет значение true
, picker
будет принимать несколько файлов и возвращать массив FileSystemFileHandle
.
Для чтения содержимого файла с помощью FileSystemFileHandle
используется метод getFile
:
const file = await fileHandle.getFile()
getFile
возвращает интерфейс File
. Для преобразования blob
в текст можно использовать метод text
(данный метод наследуется File
от интерфейса Blob
):
const fileContent = await file.text()
Предположим, что у нас имеется директория fsa-test
, в которой лежит файл test.txt
с текстом Hi World
. Прочитаем этот файл.
Пользователь должен явно выразить намерение прочитать файл или директорию, например, нажать кнопку.
Разметка:
<button class='file-picker'>File picker</button>
Скрипт:
const filePicker$ = document.querySelector('.file-picker')
filePicker$.addEventListener('click', async () => {
const [fileHandle] = await window.showOpenFilePicker()
const file = await fileHandle.getFile()
const fileContent = await file.text()
console.log(fileContent)
})
Нажимаем на кнопку. Выбираем файл test.txt
. Получаем Hi World
в консоли.
Создадим еще парочку файлов, например, test2.txt
с текстом Bye World
и test3.txt
с текстом Hi World Once Again
.
Прочитаем все 3 файла, запретив пользователю выбирать другие файлы.
Скрипт:
// настройки
const options = {
// можно выбирать несколько файлов
multiple: true,
// разрешенный тип файлов
types: [
{
description: 'Text',
accept: {
'text/plain': '.txt'
// разрешаем изображения
// 'image/*': ['.jpg', '.jpeg', '.png', '.gif']
}
}
],
// можно выбирать только разрешенные файлы
// по моим наблюдениям, данная настройка работает не совсем корректно
excludeAcceptAllOption: true
}
filePicker$.addEventListener('click', async () => {
const fileHandles = await window.showOpenFilePicker(options)
const allFilesContent = await Promise.all(
fileHandles.map(async (fileHandle) => {
const file = await fileHandle.getFile()
const fileContent = await file.text()
return fileContent
})
)
console.log(allFilesContent.join('\n'))
})
Нажимаем на кнопку. Выбираем файлы test.txt
, test2.txt
и test3.txt
. Получаем в консоли:
Hi World
Bye World
Hi World Once Again
Запись файлов
Для получения сущности для записи файла (FileSystemFileHandle
) используется метод showSaveFilePicker
:
const fileHandle = await window.showSaveFilePicker(options)
Специфичной для showSaveFilePicker
является настройка suggestedName?: string
— рекомендуемое название создаваемого файла.
Для записи файла с помощью FileSystemFileHandle
используется метод createWritable
:
const writableStream = await fileHandle.createWritable(options)
Единственной доступной на сегодняшний день настройкой createWritable
является keepExistingData?: boolean
— если имеет значение true
, picker
сохраняет данные, имеющиеся в файле на момент записи, в противном случае, содержимое файла перезаписывается.
createWritable
возвращает FileSystemWritableFileStream
, предоставляющий метод write
для записи файла:
await writableStream.write(fileData)
fileData
— это данные для записи.
Запишем файл test4.txt
с текстом Bye World Once Again
.
Разметка:
<button class="file-saver">File saver</button>
Скрипт:
const fileSaver$ = document.querySelector('.file-saver')
// настройки
const options = {
// рекомендуемое название файла
suggestedName: 'test4.txt',
types: [
{
description: 'Text',
accept: {
'text/plain': '.txt'
}
}
],
excludeAcceptAllOption: true
}
// данные для записи
const fileData = 'Bye World Once Again'
fileSaver$.addEventListener('click', async () => {
const fileHandle = await window.showSaveFilePicker(options)
const writableStream = await fileHandle.createWritable()
await writableStream.write(fileData)
// данный метод не упоминается в черновике спецификации,
// хотя там говорится о необходимости закрытия потока
// для успешной записи файла
await writableStream.close()
})
Нажимаем на кнопку File saver
. Сохраняем файл. Видим, что в директории появился файл test4.txt
с текстом Bye World Once Again
.
Перезапишем содержимое файла test.txt
.
Скрипт:
const filePicker$ = document.querySelector('.file-picker')
const fileSaver$ = document.querySelector('.file-saver')
const fileData = 'Bye World'
let fileHandle
filePicker$.addEventListener('click', async () => {
;[fileHandle] = await window.showOpenFilePicker()
const file = await fileHandle.getFile()
const fileContent = await file.text()
console.log(fileContent)
})
fileSaver$.addEventListener('click', async () => {
const writableStream = await fileHandle.createWritable({
// не работает!
keepExistingData: true
})
await writableStream.write(fileData)
await writableStream.close()
})
Нажимаем на кнопку File picker
. Выбираем файл test.txt
. Нажимаем на кнопку File saver
. Получаем уведомление о том, что браузер сможет манипулировать файлом test.txt
до закрытия всех вкладок. Нажимаем Сохранить
. Открываем test.txt
. Видим, что текст Hi World
изменился на Bye World
(текст Hi World
должен был сохраниться, поскольку мы указали настройку keepExistingData: true
).
Чтение директории
Для получения сущности для чтения директории (FileSystemDirectoryHandle
) используется метод showDirectoryPicker
:
const dirPicker = await window.showDirectoryPicker()
Для перебора содержимого выбранной директории можно использовать следующие методы:
-
entries
— возвращает массив массивов вида[name, handle]
, гдеname
— название сущности, аhandle
—FileSystemHandle
; -
values
— возвращает массивhandle
; -
keys
— возвращает массивname
.
Переместим файлы test2.txt
, test3.txt
и test4.txt
в директорию text
и прочитаем содержимое директории fsa-test
.
Структура директории fsa-test
:
- index.html
- script.js
- test.txt
- text
- test2.txt
- test3.txt
- test4.txt
Разметка:
<button class="dir-picker">Directory picker</button>
Скрипт:
const dirPicker$ = document.querySelector('.dir-picker')
dirPicker$.addEventListener('click', async () => {
const dirHandle = await window.showDirectoryPicker()
for await (const entry of dirHandle.values()) {
console.log(entry.kind, entry.name)
}
})
Нажимаем на кнопку Directory picker
. Выбираем директорию fsa-test
. Получаем уведомление о том, что наш сайт сможет просматривать файлы в выбранной директории. Нажимаем Просмотреть файлы
. Получаем в консоли:
file index.html
file script.js
file test.txt
directory text
Для получения FileSystemFileHandle
и FileSystemDirectoryHandle
, находящихся внутри выбранной директории предназначены методы getFileHandle
и getDirectoryHandle
, соответственно. Обязательным параметром, принимаемым этими методами, является name
— название файла/директории.
Прочитаем содержимое файла test.txt
и директории text
.
Скрипт:
const dirPicker$ = document.querySelector('.dir-picker')
dirPicker$.addEventListener('click', async () => {
const dirHandle = await window.showDirectoryPicker()
const fileHandle = await dirHandle.getFileHandle('test.txt')
// читаем содержимое файла `test.txt`
const fileContent = await (await fileHandle.getFile()).text()
console.log(fileContent)
// читаем содержимое директории `text`
const subDirHandle = await dirHandle.getDirectoryHandle('text')
for await (const handle of subDirHandle.values()) {
console.log(handle.kind, handle.name)
}
})
Нажимаем на кнопку Directory picker
. Выбираем директорию fsa-test
. Нажимаем Просмотреть файлы
. Получаем в консоли:
Bye World
file test4.txt
file test2.txt
file test3.txt
getFileHandle
и getDirectoryHandle
также принимают настройку create?: boolean
— если имеет значение true
, запрашиваемый файл/директория создается при отсутствии:
const fileHandle = await dirHandle.getFileHandle('test5.txt', { create: true })
Удаление файла/директории
Для удаления файла или директории предназначен метод FileSystemDirectoryHandle.removeEntry
. Он принимает 2 параметра:
-
name: string
— название удаляемого файла/директории; -
options?
— объект с настройками:-
recursive: boolean
— если имеет значениеtrue
, удаляется сама директория и все ее содержимое (данная настройка позволяет удалять непустые директории).
-
Удалим файл test.txt
и директорию text
:
const dirPicker$ = document.querySelector('.dir-picker')
dirPicker$.addEventListener('click', async () => {
const dirHandle = await window.showDirectoryPicker()
// удаляем файл `test.txt`
await dirHandle.removeEntry('test.txt')
// удаляем директорию `text`
// в ней содержатся файлы `test2.txt`, `test3.txt` и `text4.txt`,
// т.е. она является непустой
await dirHandle.removeEntry('text', { recursive: true })
})
Нажимаем на кнопку Directory picker
. Выбираем директорию fsa-test
. Получаем сразу 2 уведомления от браузера. Предоставляем ему необходимые разрешения. Видим, что файл test.txt
и директория text
благополучно удаляются.
Как видите, FSA
предоставляет в наше распоряжение довольно интересные возможности по работе с файлами и директориями, находящимися в локальной системе пользователя. Фактически он представляет собой урезанную версию модуля fs
для браузера.
Полагаю, с ростом поддержки FSA
найдет широкое применение в веб-разработке и станет прекрасным дополнением набора инструментов, включающих File API
, input type="file"
и Drag and drop API
.
Пожалуй, это все, чем я хотел поделиться с вами в этой статье.
Обратите внимание: мы рассмотрели далеко не все возможности, предоставляемые FSA
, поэтому рекомендую полистать спецификацию.
Благодарю за внимание и happy coding!
acsais-com
Разработчики браузеров запрещают JavaScript доступ к файлам из соображений безопасности. Для доступа к файлу, который может понадобиться коду JavaScript, нужно разрешение от пользователя. Причем сценарий выполняется в песочнице, в которой можно выполнять действия, связанные с Интернетом, но никак не задачи общего назначения, такие как создание файлов