Стандарт файловых систем вводит частную файловую систему источника (OPFS) в качестве конечной точки хранения, закрытой для источника страницы и невидимой для пользователя, которая предоставляет дополнительный доступ к особому типу файлов, оптимизированных для высокой производительности.
Поддержка браузеров
Исходная частная файловая система поддерживается современными браузерами и стандартизирована Рабочей группой по технологиям веб-гипертекстовых приложений ( WHATWG ) в рамках стандарта File System Living Standard .
Мотивация
Когда вы думаете о файлах на своем компьютере, вы, вероятно, представляете себе файловую иерархию: файлы, организованные в папки, которые можно просматривать с помощью проводника операционной системы. Например, в Windows для пользователя по имени Том его список дел может находиться в C:\Users\Tom\Documents\ToDo.txt . В этом примере ToDo.txt — это имя файла, а Users , Tom и Documents — имена папок. В Windows диск `C:` обозначает корневой каталог диска.
Типичный способ работы с файлами в интернете.
Для редактирования списка дел в веб-приложении обычно используется следующий порядок действий:
- Пользователь загружает файл на сервер или открывает его на клиенте с помощью
<input type="file">. - Пользователь вносит изменения, а затем загружает полученный файл с внедренным элементом
<a download="ToDo.txt>, который программно активируется с помощью функцииclick()в JavaScript. - Для открытия папок используется специальный атрибут в
<input type="file" webkitdirectory>, который, несмотря на своё проприетарное название, практически повсеместно поддерживается браузерами.
Современный способ работы с файлами в интернете.
Такой подход не отражает того, как пользователи представляют себе редактирование файлов, и в итоге пользователи получают загруженные копии своих исходных файлов. Поэтому API доступа к файловой системе ввел три метода выбора файлов:
Они обеспечивают следующий поток:
- Откройте
ToDo.txtс помощьюshowOpenFilePicker()и получите объектFileSystemFileHandle. - Чтобы
FileобъектFileSystemFileHandle, вызовите методgetFile()этого файлового дескриптора. - Измените файл, затем вызовите
requestPermission({mode: 'readwrite'})для дескриптора. - Если пользователь принимает запрос на разрешение, сохраните изменения в исходном файле.
- В качестве альтернативы вызовите
showSaveFilePicker()и позвольте пользователю выбрать новый файл. (Если пользователь выберет ранее открытый файл, его содержимое будет перезаписано.) Для повторных сохранений можно сохранить дескриптор файла, чтобы не показывать диалоговое окно сохранения файла снова.
Ограничения при работе с файлами в интернете.
Файлы и папки, доступные этими методами, находятся в так называемой видимой пользователю файловой системе. Файлы, сохраненные из интернета, и, в частности, исполняемые файлы, помечаются меткой интернета , поэтому операционная система может вывести дополнительное предупреждение перед выполнением потенциально опасного файла. В качестве дополнительной меры безопасности файлы, полученные из интернета, также защищены функцией «Безопасный просмотр» , которую для простоты и в контексте этого документа можно рассматривать как облачное сканирование на вирусы. При записи данных в файл с использованием API доступа к файловой системе запись происходит не на месте, а во временном файле. Сам файл не изменяется, если он не проходит все эти проверки безопасности.
Несмотря на внесенные улучшения, например, в macOS , эта работа делает файловые операции относительно медленными. Тем не менее, каждый вызов функции write() является самодостаточным, поэтому внутри она открывает файл, переходит к заданному смещению и, наконец, записывает данные.
Файлы как основа обработки
В то же время файлы — это отличный способ записи данных. Например, SQLite хранит целые базы данных в одном файле. Другой пример — мипмапы, используемые в обработке изображений. Мипмапы — это предварительно рассчитанные, оптимизированные последовательности изображений, каждое из которых представляет собой постепенно уменьшающееся по разрешению изображение по сравнению с предыдущим, что ускоряет многие операции, такие как масштабирование. Так как же веб-приложения могут получить преимущества файлов, но без снижения производительности, характерного для веб-ориентированной обработки файлов? Ответ — это исходная частная файловая система .
Файловая система, видимая пользователю, и исходная частная файловая система
В отличие от видимой пользователю файловой системы, просматриваемой с помощью проводника операционной системы, где файлы и папки можно читать, записывать, перемещать и переименовывать, исходная частная файловая система не предназначена для просмотра пользователями. Файлы и папки в исходной частной файловой системе, как следует из названия, являются частными, и, более конкретно, частными для источника сайта. Узнать источник страницы можно, набрав location.origin в консоли инструментов разработчика. Например, источником страницы https://developer.chrome.com/articles/ является https://developer.chrome.com . Подробнее о теории источников можно прочитать в разделе «Понимание «same-site» и «same-origin»» .
Все страницы, имеющие один и тот же источник, могут видеть данные частной файловой системы этого источника, поэтому 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);
Основной поток или веб-воркер
Существует два способа использования исходной частной файловой системы: в основном потоке или в WebWorker . WebWorker не может блокировать основной поток, что означает, что в этом контексте 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');
Определение пути к файлу или папке.
Чтобы узнать, где находится заданный файл или папка относительно указанного каталога, используйте метод 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);
}
Отладка исходной частной файловой системы
Пока не будет добавлена поддержка встроенных инструментов разработчика (см. crbug/1284595 ), используйте расширение OPFS Explorer для Chrome, чтобы отлаживать исходную частную файловую систему. Кстати, скриншот из раздела «Создание новых файлов и папок» взят непосредственно из расширения.

После установки расширения откройте инструменты разработчика Chrome, выберите вкладку «OPFS Explorer» , и вы сможете просмотреть файловую иерархию. Сохраняйте файлы из исходной частной файловой системы в видимую пользователю файловую систему, щелкая по имени файла, и удаляйте файлы и папки, щелкая по значку корзины.
Демо
Посмотрите, как работает исходная частная файловая система (если вы установите расширение OPFS Explorer) в демонстрационном примере , который использует её в качестве бэкэнда для базы данных SQLite, скомпилированной в WebAssembly. Обязательно ознакомьтесь с исходным кодом на GitHub . Обратите внимание, что встроенная версия не использует исходную частную файловую систему (поскольку iframe является кросс-доменным), но когда вы открываете демонстрационный пример в отдельной вкладке, она её использует.
Заключение
Исходная частная файловая система, разработанная WHATWG, изменила то, как мы используем файлы в интернете и взаимодействуем с ними. Она открыла новые возможности, которые были невозможны при использовании файловой системы, видимой пользователю. Все основные производители браузеров — Apple, Mozilla и Google — поддерживают эту концепцию и разделяют общее видение. Разработка исходной частной файловой системы — это в значительной степени совместная работа, и обратная связь от разработчиков и пользователей имеет важное значение для ее прогресса.
Поскольку мы продолжаем совершенствовать и улучшать стандарт, мы будем рады вашим отзывам в репозитории whatwg/fs в виде сообщений об ошибках (Issues) или запросов на слияние (Pull Requests).
Ссылки по теме
- Спецификация стандарта файловой системы
- Репозиторий стандартов файловых систем
- Статья "API файловой системы с использованием WebKit для частной файловой системы Origin"
- Расширение OPFS Explorer
Благодарности
Данный документ был проверен Остином Салли , Этьеном Ноэлем и Рэйчел Эндрю .