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 обеспечивает высокопроизводительный синхронный произвольный доступ чтения/записи. Это основная инфраструктура для создания инструментов обработки файлов в браузере.

Попробуйте эти локальные браузерные инструменты — регистрация не требуется →

#File System Access#OPFS#文件句柄#本地文件#读写权限