Стандарт файловой системы вводит частную файловую систему источника (OPFS) как конечную точку хранения, закрытую для источника страницы и не видимую пользователю, которая обеспечивает дополнительный доступ к особому типу файла, оптимизированному для высокой производительности.
Поддержка браузеров
Частная файловая система Origin поддерживается современными браузерами и стандартизирована рабочей группой по технологиям веб-гипертекстовых приложений ( WHATWG ) в File System Living Standard .
Мотивация
Когда вы думаете о файлах на компьютере, вы, вероятно, представляете себе иерархию файлов: файлы, организованные в папки, которые можно просматривать с помощью проводника операционной системы. Например, в Windows для пользователя Том список дел может находиться в C:\Users\Tom\Documents\ToDo.txt
. В этом примере ToDo.txt
— это имя файла, а Users
, Tom
и Documents
— имена папок. `C:` в Windows обозначает корневой каталог диска.
Традиционный способ работы с файлами в Интернете
Чтобы отредактировать список дел в веб-приложении, выполните следующие обычные действия:
- Пользователь загружает файл на сервер или открывает его на клиенте с помощью
<input type="file">
. - Пользователь вносит свои изменения, а затем загружает полученный файл с внедренным
<a download="ToDo.txt>
, который вы программноclick()
через JavaScript. - Для открытия папок используется специальный атрибут в
<input type="file" webkitdirectory>
, который, несмотря на свое фирменное название, имеет практически универсальную поддержку браузеров.
Современный способ работы с файлами в Интернете
Этот процесс не отражает того, как пользователи представляют себе редактирование файлов, и означает, что пользователи в конечном итоге получают загруженные копии своих входных файлов. Поэтому в API доступа к файловой системе были введены три метода выбора showOpenFilePicker()
, showSaveFilePicker()
и showDirectoryPicker()
, — которые выполняют именно то, что следует из их названий. Они реализуют следующий процесс:
- Откройте
ToDo.txt
с помощьюshowOpenFilePicker()
и получите объектFileSystemFileHandle
. - Из объекта
FileSystemFileHandle
получитеFile
, вызвав методgetFile()
дескриптора файла. - Измените файл, затем вызовите
requestPermission({mode: 'readwrite'})
для дескриптора. - Если пользователь принимает запрос на разрешение, сохраните изменения в исходном файле.
- В качестве альтернативы можно вызвать
showSaveFilePicker()
и позволить пользователю выбрать новый файл. (Если пользователь выберет ранее открытый файл, его содержимое будет перезаписано.) При повторных сохранениях можно сохранить дескриптор файла, чтобы не отображать диалоговое окно сохранения файла снова.
Ограничения работы с файлами в Интернете
Файлы и папки, доступные с помощью этих методов, находятся в так называемой видимой пользователю файловой системе. Файлы, сохранённые из Интернета, и в частности исполняемые файлы, помечаются меткой веба , поэтому операционная система может вывести дополнительное предупреждение перед запуском потенциально опасного файла. В качестве дополнительной функции безопасности файлы, полученные из Интернета, также защищены функцией Safe Browsing , которую для простоты и в контексте данной статьи можно рассматривать как облачное антивирусное сканирование. При записи данных в файл с помощью API доступа к файловой системе запись производится не на место, а во временный файл. Сам файл не изменяется, пока не пройдёт все эти проверки безопасности. Как вы можете себе представить, это делает файловые операции относительно медленными, несмотря на улучшения, применённые там, где это возможно, например, в macOS . Тем не менее, каждый вызов write()
является самостоятельным, поэтому внутри он открывает файл, ищет заданное смещение и, наконец, записывает данные.
Файлы как основа обработки
В то же время файлы — отличный способ записи данных. Например, SQLite хранит целые базы данных в одном файле. Другой пример — MIP-текстуры , используемые в обработке изображений. MIP-текстуры — это предварительно рассчитанные, оптимизированные последовательности изображений, каждое из которых представляет собой представление предыдущего с постепенно уменьшающимся разрешением, что ускоряет многие операции, например, масштабирование. Как же веб-приложения могут использовать преимущества файлов, избежав при этом потерь производительности, связанных с веб-обработкой файлов? Ответ — частная файловая система Origin .
Видимая пользователем и исходная частная файловая система
В отличие от видимой пользователю файловой системы, просматриваемой через проводник операционной системы, где файлы и папки можно читать, записывать, перемещать и переименовывать, частная файловая система источника не предназначена для просмотра пользователями. Файлы и папки в частной файловой системе источника, как следует из названия, являются частными, а точнее, частными по отношению к источнику сайта. Узнать источник страницы можно, введя location.origin
в консоли DevTools. Например, источником страницы https://developer.chrome.com/articles/
является https://developer.chrome.com
(то есть часть /articles
не является частью источника). Подробнее о теории происхождения можно узнать в разделе «Understanding "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()
. Для сравнения видимой пользователю файловой системы и приватной файловой системы источника см. следующую диаграмму. Диаграмма показывает, что за исключением корневого каталога, все остальное концептуально одинаково, с иерархией файлов и папок, которые можно организовать и расположить в соответствии с вашими потребностями в данных и хранилище.
Особенности исходной частной файловой системы
Как и другие механизмы хранения в браузере (например, 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);
Основной поток или Web Worker
Существует два способа использования исходной частной файловой системы: в основном потоке или в Web Worker . Web Worker не может блокировать основной поток, что означает, что в этом контексте API могут быть синхронными, что обычно запрещено в основном потоке. Синхронные API могут быть быстрее, поскольку им не приходится работать с обещаниями, а файловые операции обычно выполняются синхронно в таких языках, как C, которые можно скомпилировать в WebAssembly.
// This is synchronous C code.
FILE *f;
f = fopen("example.txt", "w+");
fputs("Some text\n", f);
fclose(f);
Если вам нужны максимально быстрые файловые операции или вы работаете с WebAssembly , перейдите к разделу «Использование исходной частной файловой системы в Web Worker» . В противном случае продолжайте читать.
Использовать исходную частную файловую систему в основном потоке
Создание новых файлов и папок
После создания корневой папки создайте файлы и папки с помощью методов 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;
};
Использовать исходную частную файловую систему в Web Worker
Как уже говорилось, Web Workers не могут блокировать основной поток, поэтому в данном контексте разрешены синхронные методы.
Получение синхронного дескриптора доступа
Точкой входа для максимально быстрых файловых операций является 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()
доступен только в основном потоке, но не в потоке Worker, обязательно запустите код там.
// 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 ), используйте расширение OPFS Explorer для Chrome для отладки приватной файловой системы Origin. Кстати, скриншот выше из раздела «Создание новых файлов и папок» взят прямо из этого расширения.
После установки расширения откройте Chrome DevTools, выберите вкладку OPFS Explorer , и вы будете готовы к проверке файловой иерархии. Сохраняйте файлы из исходной приватной файловой системы в видимой пользователю файловой системе, щёлкнув по имени файла, и удаляйте файлы и папки, щёлкнув по значку корзины.
Демо
Посмотрите на работу приватной файловой системы Origin (если установлено расширение OPFS Explorer) в демо-версии , которая использует её в качестве бэкенда для базы данных SQLite, скомпилированной в WebAssembly. Обязательно ознакомьтесь с исходным кодом на Glitch . Обратите внимание, что встроенная версия ниже не использует бэкенд приватной файловой системы Origin (поскольку iframe кросс-доменный), но при открытии демо-версии в отдельной вкладке она используется.
Выводы
Приватная файловая система Origin, разработанная WHATWG, сформировала наш подход к использованию и взаимодействию с файлами в Интернете. Она открыла новые возможности, которые были невозможны при использовании файловой системы, доступной пользователю. Все основные производители браузеров — Apple, Mozilla и Google — поддерживают эту идею и разделяют общее видение. Разработка приватной файловой системы Origin — это во многом совместный проект, и отзывы разработчиков и пользователей играют ключевую роль в её развитии. По мере того, как мы продолжаем совершенствовать стандарт, мы будем рады обратной связи в репозитории whatwg/fs в виде сообщений об ошибках или запросов на извлечение.
Ссылки по теме
- Стандартная спецификация файловой системы
- Репозиторий File System Standard
- API файловой системы с записью Origin Private File System WebKit
- Расширение OPFS Explorer
Благодарности
Рецензенты статьи: Остин Салли , Этьен Ноэль и Рэйчел Эндрю . Изображение предоставлено Кристиной Рампф на Unsplash .