Привет, друзья!


В этой небольшой статье я хочу рассказать вам о 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 — название сущности, а handleFileSystemHandle;
  • 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!




Комментарии (1)


  1. acsais-com
    01.03.2022 13:12

    Разработчики браузеров запрещают JavaScript доступ к файлам из соображений безопасности. Для доступа к файлу, который может понадобиться коду JavaScript, нужно разрешение от пользователя. Причем сценарий выполняется в песочнице, в которой можно выполнять действия, связанные с Интернетом, но никак не задачи общего назначения, такие как создание файлов