Исходная частная файловая система

Стандарт файловой системы представляет исходную частную файловую систему (OPFS) как конечную точку хранения, приватную для источника страницы и невидимую для пользователя, которая обеспечивает дополнительный доступ к файлу особого типа, который высоко оптимизирован для производительности.

Поддержка браузера

Исходная частная файловая система поддерживается современными браузерами и стандартизирована Рабочей группой по технологиям веб-гипертекстовых приложений ( WHATWG ) в « Живом стандарте файловой системы» .

Поддержка браузера

  • Хром: 86.
  • Край: 86.
  • Фаерфокс: 111.
  • Сафари: 15.2.

Источник

Мотивация

Когда вы думаете о файлах на своем компьютере, вы, вероятно, думаете об иерархии файлов: файлы организованы в папки, которые вы можете просмотреть с помощью проводника вашей операционной системы. Например, в Windows список дел пользователя по имени Том может находиться в C:\Users\Tom\Documents\ToDo.txt . В этом примере ToDo.txt — это имя файла, а Users , Tom и Documents — имена папок. `C:` в Windows представляет корневой каталог диска.

Традиционный способ работы с файлами в Интернете

Чтобы отредактировать список дел в веб-приложении, выполните обычный процесс:

  1. Пользователь загружает файл на сервер или открывает его на клиенте с помощью <input type="file"> .
  2. Пользователь вносит свои изменения, а затем загружает полученный файл с добавленным <a download="ToDo.txt> , который вы программно click() через JavaScript.
  3. Для открытия папок вы используете специальный атрибут в <input type="file" webkitdirectory> , который, несмотря на свое фирменное название, имеет практически универсальную поддержку браузеров.

Современный способ работы с файлами в Интернете

Этот процесс не отражает того, как пользователи думают о редактировании файлов, и означает, что в конечном итоге пользователи получают загруженные копии своих входных файлов. Поэтому API доступа к файловой системе представил три метода выбора — showOpenFilePicker() , showSaveFilePicker() и showDirectoryPicker() — которые делают именно то, что предполагает их название. Они включают поток следующим образом:

  1. Откройте ToDo.txt с помощью showOpenFilePicker() и получите объект FileSystemFileHandle .
  2. Из объекта FileSystemFileHandle получите File , вызвав метод getFile() дескриптора файла.
  3. Измените файл, затем вызовите requestPermission({mode: 'readwrite'}) для дескриптора.
  4. Если пользователь принимает запрос на разрешение, сохраните изменения обратно в исходный файл.
  5. Альтернативно, вызовите showSaveFilePicker() и позвольте пользователю выбрать новый файл. (Если пользователь выберет ранее открытый файл, его содержимое будет перезаписано.) Для повторного сохранения вы можете сохранить дескриптор файла, чтобы вам не приходилось снова показывать диалоговое окно сохранения файла.

Ограничения работы с файлами в сети

Файлы и папки, доступные с помощью этих методов, находятся в так называемой файловой системе , видимой пользователю . Файлы, сохраненные из Интернета, и в частности исполняемые файлы, помечены знаком Интернета , поэтому операционная система может показать дополнительное предупреждение перед выполнением потенциально опасного файла. В качестве дополнительной функции безопасности файлы, полученные из Интернета, также защищаются с помощью безопасного просмотра , который для простоты и в контексте этой статьи можно рассматривать как облачное сканирование на вирусы. Когда вы записываете данные в файл с помощью API доступа к файловой системе, запись выполняется не на месте, а с использованием временного файла. Сам файл не изменяется, пока не пройдет все эти проверки безопасности. Как вы понимаете, такая работа делает файловые операции относительно медленными, несмотря на улучшения, примененные там, где это возможно, например, в macOS . Тем не менее, каждый вызов write() является автономным, поэтому внутри он открывает файл, ищет заданное смещение и, наконец, записывает данные.

Файлы как основа обработки

В то же время файлы являются отличным способом записи данных. Например, SQLite хранит целые базы данных в одном файле. Другим примером являются MIP-карты, используемые при обработке изображений. Mip-карты — это заранее рассчитанные оптимизированные последовательности изображений, каждое из которых представляет собой представление предыдущего с прогрессивно более низким разрешением, что ускоряет многие операции, такие как масштабирование. Так как же веб-приложения могут получить преимущества файлов, но без затрат на производительность, связанных с обработкой файлов через Интернет? Ответ — исходная частная файловая система .

Видимая пользователю и исходная частная файловая система

В отличие от видимой пользователю файловой системы, просматриваемой с помощью проводника операционной системы, в которой файлы и папки можно читать, записывать, перемещать и переименовывать, исходная частная файловая система не предназначена для просмотра пользователями. Файлы и папки в исходной частной файловой системе, как следует из названия, являются частными, а точнее, частными по отношению к исходному сайту. Узнайте происхождение страницы, набрав location.origin в консоли DevTools. Например, источником страницы https://developer.chrome.com/articles/ является https://developer.chrome.com (то есть часть /articles не является частью источника). Подробнее о теории происхождения можно прочитать в разделе Понимание «одного и того же места» и «одного и того же происхождения» . Все страницы, имеющие один и тот же источник, могут видеть одни и те же данные частной файловой системы происхождения, поэтому https://developer.chrome.com/docs/extensions/mv3/getstarted/extensions-101/ могут видеть те же сведения, что и в предыдущем примере. Каждый источник имеет свою собственную независимую частную файловую систему происхождения, что означает, что частная файловая система происхождения https://developer.chrome.com полностью отличается от, скажем, https://web.dev . В Windows корневым каталогом видимой пользователю файловой системы является C:\\ . Эквивалентом частной файловой системы источника является изначально пустой корневой каталог для каждого источника, доступ к которому осуществляется путем вызова асинхронного метода navigator.storage.getDirectory() . Для сравнения видимой пользователю файловой системы и исходной частной файловой системы см. следующую диаграмму. На диаграмме показано, что, за исключением корневого каталога, все остальное концептуально одинаково, с иерархией файлов и папок, которую можно организовать и упорядочить по мере необходимости в соответствии с вашими потребностями в данных и хранилище.

Схема видимой пользователю файловой системы и исходной частной файловой системы с двумя примерными файловыми иерархиями. Точкой входа в видимую пользователю файловую систему является символический жесткий диск, а точкой входа в исходную частную файловую систему является вызов метода navigator.storage.getDirectory.

Особенности происхождения частной файловой системы

Как и другие механизмы хранения в браузере (например, localStorage или IndexedDB ), исходная частная файловая система подлежит ограничениям квот браузера. Когда пользователь удаляет все данные просмотра или все данные сайта , исходная частная файловая система также будет удалена. Вызовите navigator.storage.estimate() и в полученном объекте ответа просмотрите запись usage , чтобы узнать, сколько памяти уже потребляет ваше приложение, которая разбита по механизму хранения в объекте usageDetails , где вы хотите конкретно просмотреть запись fileSystem . Поскольку исходная частная файловая система не видна пользователю, запросы на получение разрешений и проверки безопасного просмотра отсутствуют.

Получение доступа к корневому каталогу

Чтобы получить доступ к корневому каталогу, выполните следующую команду. В итоге вы получите пустой дескриптор каталога, точнее, FileSystemDirectoryHandle .

const opfsRoot = await navigator.storage.getDirectory();
// A FileSystemDirectoryHandle whose type is "directory"
// and whose name is "".
console.log(opfsRoot);

Основной поток или веб-воркер

Существует два способа использования исходной частной файловой системы: в основном потоке или в веб-воркере . Веб-воркеры не могут блокировать основной поток, что означает, что в этом контексте API могут быть синхронными, что обычно запрещено в основном потоке. Синхронные API могут работать быстрее, поскольку им не приходится иметь дело с обещаниями, а файловые операции обычно синхронны в таких языках, как C, которые можно скомпилировать в WebAssembly.

// This is synchronous C code.
FILE *f;
f = fopen("example.txt", "w+");
fputs("Some text\n", f);
fclose(f);

Если вам нужны максимально быстрые файловые операции или вы имеете дело с WebAssembly , перейдите к разделу «Использовать исходную частную файловую систему в веб-воркере» . В противном случае вы можете читать дальше.

Используйте исходную частную файловую систему в основном потоке.

Создание новых файлов и папок

Если у вас есть корневая папка, создайте файлы и папки, используя методы getFileHandle() и getDirectoryHandle() соответственно. При передаче {create: true} файл или папка будут созданы, если они не существуют. Создайте иерархию файлов, вызывая эти функции, используя вновь созданный каталог в качестве отправной точки.

const fileHandle = await opfsRoot
    .getFileHandle('my first file', {create: true});
const directoryHandle = await opfsRoot
    .getDirectoryHandle('my first folder', {create: true});
const nestedFileHandle = await directoryHandle
    .getFileHandle('my first nested file', {create: true});
const nestedDirectoryHandle = await directoryHandle
    .getDirectoryHandle('my first nested folder', {create: true});

Результирующая иерархия файлов из предыдущего примера кода.

Доступ к существующим файлам и папкам

Если вы знаете их имя, получите доступ к ранее созданным файлам и папкам, вызвав методы getFileHandle() или getDirectoryHandle() , передав имя файла или папки.

const existingFileHandle = await opfsRoot.getFileHandle('my first file');
const existingDirectoryHandle = await opfsRoot
    .getDirectoryHandle('my first folder');

Получение файла, связанного с дескриптором файла, для чтения

FileSystemFileHandle представляет файл в файловой системе. Чтобы получить связанный File , используйте метод getFile() . Объект File представляет собой особый вид Blob и может использоваться в любом контексте, в котором может использоваться Blob . В частности, FileReader , URL.createObjectURL() , createImageBitmap() и XMLHttpRequest.send() принимают как Blobs , так и Files . Если хотите, получение File из FileSystemFileHandle «освобождает» данные, поэтому вы можете получить к ним доступ и сделать их доступными для видимой пользователю файловой системы.

const file = await fileHandle.getFile();
console.log(await file.text());

Запись в файл посредством потоковой передачи

Передавайте данные в файл, вызывая createWritable() , который создает FileSystemWritableFileStream , в который вы затем write() содержимое. В конце вам нужно close() поток.

const contents = 'Some text';
// Get a writable stream.
const writable = await fileHandle.createWritable();
// Write the contents of the file to the stream.
await writable.write(contents);
// Close the stream, which persists the contents.
await writable.close();

Удалить файлы и папки

Удаляйте файлы и папки, вызывая специальный метод remove() их дескриптора файла или каталога. Чтобы удалить папку, включая все подпапки, передайте параметр {recursive: true} .

await fileHandle.remove();
await directoryHandle.remove({recursive: true});

В качестве альтернативы, если вы знаете имя удаляемого файла или папки в каталоге, используйте метод removeEntry() .

directoryHandle.removeEntry('my first nested file');

Перемещение и переименование файлов и папок

Переименовывайте и перемещайте файлы и папки с помощью метода move() . Перемещение и переименование могут происходить вместе или по отдельности.

// Rename a file.
await fileHandle.move('my first renamed file');
// Move a file to another directory.
await fileHandle.move(nestedDirectoryHandle);
// Move a file to another directory and rename it.
await fileHandle
    .move(nestedDirectoryHandle, 'my first renamed and now nested file');

Определить путь к файлу или папке

Чтобы узнать, где находится данный файл или папка относительно ссылочного каталога, используйте методsolve resolve() , передав ему FileSystemHandle в качестве аргумента. Чтобы получить полный путь к файлу или папке в исходной частной файловой системе, используйте корневой каталог в качестве ссылочного каталога, полученного с помощью navigator.storage.getDirectory() .

const relativePath = await opfsRoot.resolve(nestedDirectoryHandle);
// `relativePath` is `['my first folder', 'my first nested folder']`.

Проверьте, указывают ли два дескриптора файла или папки на один и тот же файл или папку.

Иногда у вас есть два дескриптора, и вы не знаете, указывают ли они на один и тот же файл или папку. Чтобы проверить, так ли это, используйте метод isSameEntry() .

fileHandle.isSameEntry(nestedFileHandle);
// Returns `false`.

Перечислить содержимое папки

FileSystemDirectoryHandle — это асинхронный итератор , который вы выполняете с помощью цикла for await…of . Как асинхронный итератор он также поддерживает методы entries() , values() keys() , из которых вы можете выбирать в зависимости от того, какая информация вам нужна:

for await (let [name, handle] of directoryHandle) {}
for await (let [name, handle] of directoryHandle.entries()) {}
for await (let handle of directoryHandle.values()) {}
for await (let name of directoryHandle.keys()) {}

Рекурсивно перечислить содержимое папки и всех подпапок

Имея дело с асинхронными циклами и функциями в сочетании с рекурсией, легко ошибиться. Приведенная ниже функция может служить отправной точкой для вывода списка содержимого папки и всех ее подпапок, включая все файлы и их размеры. Вы можете упростить функцию, если вам не нужны размеры файлов, где написано directoryEntryPromises.push , не отправляя обещание handle.getFile() , а непосредственно handle .

  const getDirectoryEntriesRecursive = async (
    directoryHandle,
    relativePath = '.',
  ) => {
    const fileHandles = [];
    const directoryHandles = [];
    const entries = {};
    // Get an iterator of the files and folders in the directory.
    const directoryIterator = directoryHandle.values();
    const directoryEntryPromises = [];
    for await (const handle of directoryIterator) {
      const nestedPath = `${relativePath}/${handle.name}`;
      if (handle.kind === 'file') {
        fileHandles.push({ handle, nestedPath });
        directoryEntryPromises.push(
          handle.getFile().then((file) => {
            return {
              name: handle.name,
              kind: handle.kind,
              size: file.size,
              type: file.type,
              lastModified: file.lastModified,
              relativePath: nestedPath,
              handle
            };
          }),
        );
      } else if (handle.kind === 'directory') {
        directoryHandles.push({ handle, nestedPath });
        directoryEntryPromises.push(
          (async () => {
            return {
              name: handle.name,
              kind: handle.kind,
              relativePath: nestedPath,
              entries:
                  await getDirectoryEntriesRecursive(handle, nestedPath),
              handle,
            };
          })(),
        );
      }
    }
    const directoryEntries = await Promise.all(directoryEntryPromises);
    directoryEntries.forEach((directoryEntry) => {
      entries[directoryEntry.name] = directoryEntry;
    });
    return entries;
  };

Используйте исходную частную файловую систему в веб-воркере.

Как отмечалось ранее, веб-воркеры не могут блокировать основной поток, поэтому в этом контексте разрешены синхронные методы.

Получение дескриптора синхронного доступа

Точкой входа для самых быстрых файловых операций является FileSystemSyncAccessHandle , полученная из обычного FileSystemFileHandle путем вызова createSyncAccessHandle() .

const fileHandle = await opfsRoot
    .getFileHandle('my highspeed file.txt', {create: true});
const syncAccessHandle = await fileHandle.createSyncAccessHandle();

Синхронные методы файлов на месте

Имея дескриптор синхронного доступа, вы получаете доступ к быстрым локальным файловым методам, которые являются синхронными.

  • getSize() : возвращает размер файла в байтах.
  • write() : записывает содержимое буфера в файл (необязательно с заданным смещением) и возвращает количество записанных байтов. Проверка возвращенного количества записанных байтов позволяет вызывающей стороне обнаруживать и обрабатывать ошибки и частичную запись.
  • read() : считывает содержимое файла в буфер, возможно, по заданному смещению.
  • truncate() : изменяет размер файла до заданного размера.
  • flush() : гарантирует, что содержимое файла содержит все изменения, сделанные с помощью write() .
  • close() : закрывает дескриптор доступа.

Вот пример, в котором используются все методы, упомянутые выше.

const opfsRoot = await navigator.storage.getDirectory();
const fileHandle = await opfsRoot.getFileHandle('fast', {create: true});
const accessHandle = await fileHandle.createSyncAccessHandle();

const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();

// Initialize this variable for the size of the file.
let size;
// The current size of the file, initially `0`.
size = accessHandle.getSize();
// Encode content to write to the file.
const content = textEncoder.encode('Some text');
// Write the content at the beginning of the file.
accessHandle.write(content, {at: size});
// Flush the changes.
accessHandle.flush();
// The current size of the file, now `9` (the length of "Some text").
size = accessHandle.getSize();

// Encode more content to write to the file.
const moreContent = textEncoder.encode('More content');
// Write the content at the end of the file.
accessHandle.write(moreContent, {at: size});
// Flush the changes.
accessHandle.flush();
// The current size of the file, now `21` (the length of
// "Some textMore content").
size = accessHandle.getSize();

// Prepare a data view of the length of the file.
const dataView = new DataView(new ArrayBuffer(size));

// Read the entire file into the data view.
accessHandle.read(dataView);
// Logs `"Some textMore content"`.
console.log(textDecoder.decode(dataView));

// Read starting at offset 9 into the data view.
accessHandle.read(dataView, {at: 9});
// Logs `"More content"`.
console.log(textDecoder.decode(dataView));

// Truncate the file after 4 bytes.
accessHandle.truncate(4);

Скопируйте файл из исходной частной файловой системы в видимую пользователю файловую систему.

Как упоминалось выше, перемещение файлов из исходной частной файловой системы в видимую пользователю файловую систему невозможно, но вы можете копировать файлы. Поскольку showSaveFilePicker() доступен только в основном потоке, но не в рабочем потоке, обязательно запускайте код там.

// On the main thread, not in the Worker. This assumes
// `fileHandle` is the `FileSystemFileHandle` you obtained
// the `FileSystemSyncAccessHandle` from in the Worker
// thread. Be sure to close the file in the Worker thread first.
const fileHandle = await opfsRoot.getFileHandle('fast');
try {
  // Obtain a file handle to a new file in the user-visible file system
  // with the same name as the file in the origin private file system.
  const saveHandle = await showSaveFilePicker({
    suggestedName: fileHandle.name || ''
  });
  const writable = await saveHandle.createWritable();
  await writable.write(await fileHandle.getFile());
  await writable.close();
} catch (err) {
  console.error(err.name, err.message);
}

Отладка исходной частной файловой системы

До тех пор, пока не будет добавлена ​​встроенная поддержка DevTools (см. crbug/1284595 ), используйте расширение Chrome OPFS Explorer для отладки исходной частной файловой системы. Скриншот выше из раздела Создание новых файлов и папок, кстати, взят прямо из расширения.

Расширение OPFS Explorer Chrome DevTools в Интернет-магазине Chrome.

После установки расширения откройте Chrome DevTools, выберите вкладку OPFS Explorer , и вы будете готовы проверить иерархию файлов. Сохраните файлы из исходной частной файловой системы в видимую пользователю файловую систему, щелкнув имя файла, и удалите файлы и папки, щелкнув значок корзины.

Демо

Посмотрите исходную частную файловую систему в действии (если вы установили расширение OPFS Explorer) в демонстрационной версии , которая использует ее в качестве серверной части для базы данных SQLite, скомпилированной в WebAssembly. Обязательно ознакомьтесь с исходным кодом на Glitch . Обратите внимание, что встроенная версия ниже не использует исходную внутреннюю файловую систему (поскольку iframe имеет перекрестное происхождение), но когда вы открываете демо-версию на отдельной вкладке, она использует.

Выводы

Исходная частная файловая система, как указано WHATWG, сформировала то, как мы используем файлы в Интернете и взаимодействуем с ними. Это позволило реализовать новые варианты использования, которые невозможно было реализовать с помощью видимой пользователю файловой системы. Все основные поставщики браузеров — Apple, Mozilla и Google — участвуют в проекте и разделяют общее видение. Разработка исходной частной файловой системы — это во многом совместная работа, и для ее прогресса важна обратная связь от разработчиков и пользователей. Поскольку мы продолжаем совершенствовать и совершенствовать стандарт, приветствуются отзывы о репозитории Whatwg/fs в форме проблем или запросов на извлечение.

Благодарности

Эта статья была рецензирована Остином Салли , Этьеном Ноэлем и Рэйчел Эндрю . Изображение героя Кристины Румпф на Unsplash .