File System Access API на практике: Архитектура чтения/записи локальных файлов в браузере
Эволюция доступа к файлам в браузере
| Подход | Чтение/Запись | Сохранение | Модель разрешений | Производительность |
|---|---|---|---|---|
<input type="file"> |
Только чтение | ❌ | Нет | Средняя |
| File + FileReader | Только чтение | ❌ | Нет | Средняя |
| IndexedDB | Чтение/запись байтов | ✅ | Тот же источник | Низкая (сериализация) |
| File System Access | Чтение/запись | ✅ | Предоставлено пользователем | Высокая |
| OPFS | Чтение/запись | ✅ | Песочница того же источника | Наивысшая |
File System Access API предоставляет браузерам настоящие возможности чтения/записи файлов на уровне нативных приложений.
Выбор файлов: showOpenFilePicker / showSaveFilePicker
Открытие файла
const [fileHandle] = await window.showOpenFilePicker({
types: [{
description: 'Image files',
accept: { 'image/*': ['.png', '.jpg', '.webp'] },
}],
multiple: false,
});
const file = await fileHandle.getFile();
const arrayBuffer = await file.arrayBuffer();
Сохранение файла
const handle = await window.showSaveFilePicker({
suggestedName: 'compressed.webp',
types: [{
description: 'WebP Image',
accept: { 'image/webp': ['.webp'] },
}],
});
const writable = await handle.createWritable();
await writable.write(compressedBlob);
await writable.close();
FileSystemFileHandle: Чтение/запись и разрешения
Чтение содержимого файла
const fileHandle: FileSystemFileHandle = handle;
const file = await fileHandle.getFile();
// Multiple read methods
const text = await file.text(); // Text
const buffer = await file.arrayBuffer(); // Binary
const stream = file.stream(); // Streaming
Запись в файл
async function writeFile(handle: FileSystemFileHandle, content: Blob | string | BufferSource) {
const writable = await handle.createWritable();
await writable.write(content);
await writable.close();
}
Запрос и получение разрешений
async function verifyPermission(handle: FileSystemFileHandle, readWrite = true) {
const options: FileSystemHandlePermissionDescriptor = { mode: readWrite ? 'readwrite' : 'read' };
if ((await handle.queryPermission(options)) === 'granted') return true;
if ((await handle.requestPermission(options)) === 'granted') return true;
return false;
}
Модель разрешений: Дескрипторы, полученные через выборщик, автоматически получают разрешение на чтение. Разрешение на запись требует явной авторизации пользователя (requestPermission отображает запрос).
Доступ к каталогам и обход
const dirHandle = await window.showDirectoryPicker();
for await (const [name, handle] of dirHandle.entries()) {
if (handle.kind === 'file') {
const file = await handle.getFile();
console.log(`File: ${name}, Size: ${file.size}`);
} else if (handle.kind === 'directory') {
console.log(`Directory: ${name}`);
}
}
Рекурсивный поиск файлов
async function* findFiles(dirHandle: FileSystemDirectoryHandle, pattern: RegExp) {
for await (const [name, handle] of dirHandle.entries()) {
if (handle.kind === 'file' && pattern.test(name)) {
yield { name, handle };
} else if (handle.kind === 'directory') {
yield* findFiles(handle, pattern);
}
}
}
// Find all PDF files
for await (const pdf of findFiles(dirHandle, /\.pdf$/)) {
console.log(pdf.name);
}
Origin Private File System (OPFS)
OPFS — это частная изолированная файловая система, предоставляемая браузером для каждого источника—авторизация пользователя не требуется:
const opfsRoot = await navigator.storage.getDirectory();
// Create a file
const fileHandle = await opfsRoot.getFileHandle('work-data.bin', { create: true });
// Create a subdirectory
const subDir = await opfsRoot.getDirectoryHandle('cache', { create: true });
// Delete a file
await opfsRoot.removeEntry('work-data.bin');
OPFS vs IndexedDB
| Характеристика | IndexedDB | OPFS |
|---|---|---|
| Тип хранилища | Структурированные данные | Файлы (двоичные) |
| Режим чтения/записи | Транзакции + сериализация | Прямой файловый ввод-вывод |
| Производительность с большими файлами | Низкая (накладные расходы сериализации) | Отличная |
| Произвольный доступ | ❌ | ✅ (Access Handle) |
| Ёмкость | Лимит того же источника | Лимит того же источника |
| Поддержка Workers | ✅ | ✅ |
Access Handle: Высокопроизводительный произвольный доступ чтения/записи
Файлы OPFS поддерживают createSyncAccessHandle(), обеспечивающий синхронный позиционный доступ чтения/записи—производительность, близкая к нативной файловой системе:
const fileHandle = await opfsRoot.getFileHandle('large-data.bin', { create: true });
const accessHandle = await fileHandle.createSyncAccessHandle();
// Write data at a specific position
const writeBuffer = new Uint8Array([1, 2, 3, 4, 5]);
accessHandle.write(writeBuffer, { at: 0 });
// Read data at a specific position
const readBuffer = new Uint8Array(5);
accessHandle.read(readBuffer, { at: 0 });
// Get file size
const fileSize = accessHandle.getSize();
// Flush to disk
accessHandle.flush();
// Close handle
accessHandle.close();
Ключевое преимущество: Access Handle синхронный, может использоваться в Workers без async/await—в 10-100 раз быстрее WritableStream.
Практика: Архитектура чтения/записи файлов для объединения PDF
Объединение PDF от ToolsKu использует File System Access API для полного рабочего процесса «открыть → обработать → сохранить»:
async function mergePdfWorkflow() {
// 1. User selects multiple PDFs
const handles = await window.showOpenFilePicker({
types: [{ accept: { 'application/pdf': ['.pdf'] } }],
multiple: true,
});
// 2. Read all files
const buffers: ArrayBuffer[] = [];
for (const handle of handles) {
const file = await handle.getFile();
buffers.push(await file.arrayBuffer());
}
// 3. Merge processing
const mergedPdf = await mergePdfs(buffers);
// 4. Save result
const saveHandle = await window.showSaveFilePicker({
suggestedName: 'merged.pdf',
types: [{ accept: { 'application/pdf': ['.pdf'] } }],
});
const writable = await saveHandle.createWritable();
await writable.write(mergedPdf);
await writable.close();
}
Сохранение файловых дескрипторов
Сохраняйте дескрипторы в IndexedDB для восстановления доступа при следующем посещении:
async function saveHandle(key: string, handle: FileSystemFileHandle) {
const db = await openDB('file-handles', 1, {
upgrade(db) { db.createObjectStore('handles'); },
});
await db.put('handles', handle, key);
}
async function loadHandle(key: string) {
const db = await openDB('file-handles', 1);
const handle: FileSystemFileHandle = await db.get('handles', key);
if (await verifyPermission(handle, true)) {
return handle;
}
return null;
}
Часто задаваемые вопросы
Поддержка браузерами File System Access API?
Chrome/Edge 86+ полностью поддерживают. Firefox и Safari не поддерживают. Требуется определение функций и обработка откатов.
Ограничения ёмкости хранилища OPFS?
OPFS разделяет квоту хранилища того же источника с Cache API и IndexedDB (обычно 50%+ доступного дискового пространства). Запросите через navigator.storage.estimate().
Можно ли использовать Access Handle в основном потоке?
Да, но синхронный ввод-вывод блокирует основной поток. Лучшая практика: используйте Access Handle в Worker, чтобы не блокировать пользовательский интерфейс.
Как обрабатывать запись больших файлов?
Используйте поблочную запись через WritableStream, чтобы избежать загрузки всего файла в память:
const writable = await handle.createWritable();
for await (const chunk of readableStream) {
await writable.write(chunk);
}
await writable.close();
Итоги
File System Access API предоставляет браузерам возможности чтения/записи файлов на уровне нативных приложений. showOpenFilePicker/showSaveFilePicker обеспечивают авторизованный пользователем доступ к файлам, FileSystemFileHandle управляет разрешениями чтения/записи, OPFS предоставляет изолированное хранилище без авторизации, а Access Handle обеспечивает высокопроизводительный синхронный произвольный доступ чтения/записи. Это основная инфраструктура для создания инструментов обработки файлов в браузере.
Попробуйте эти локальные браузерные инструменты — регистрация не требуется →