File System Access API in Practice: Browser-Side Local File Read/Write Architecture

技术架构(Updated Jun 15, 2026)

The Evolution of Browser File Access

Approach Read/Write Persistence Permission Model Performance
<input type="file"> Read-only None Medium
File + FileReader Read-only None Medium
IndexedDB Read/write bytes Same-origin Low (serialization)
File System Access Read/write User-granted High
OPFS Read/write Same-origin sandbox Highest

The File System Access API gives browsers true native-app-level file read/write capabilities.


File Pickers: showOpenFilePicker / showSaveFilePicker

Open a File

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();

Save a File

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: Read/Write and Permissions

Reading File Content

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

Writing to a File

async function writeFile(handle: FileSystemFileHandle, content: Blob | string | BufferSource) {
  const writable = await handle.createWritable();
  await writable.write(content);
  await writable.close();
}

Permission Query and Request

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;
}

Permission model: Handles obtained via picker automatically get read permission. Write permission requires explicit user authorization (requestPermission shows a prompt).


Directory Access and Traversal

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 is a private sandboxed file system provided by the browser for each origin—no user authorization needed:

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

Feature IndexedDB OPFS
Storage type Structured data Files (binary)
Read/write mode Transactions + serialization Direct file I/O
Large file performance Poor (serialization overhead) Excellent
Random access ✅ (Access Handle)
Capacity Same-origin limit Same-origin limit
Worker support

Access Handle: High-Performance Random Read/Write

OPFS files support createSyncAccessHandle(), providing synchronous, position-based file read/write—performance close to native filesystem:

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();

Key advantage: Access Handle is synchronous, usable in Workers without async/await—10-100x faster than WritableStream.


Practice: PDF Merge File Read/Write Architecture

ToolsKu's PDF Merge uses the File System Access API for a complete "open → process → save" workflow:

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();
}

Persisting File Handles

Save handles in IndexedDB to restore access on next visit:

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;
}

Common Questions

Browser support for File System Access API?

Chrome/Edge 86+ have full support. Firefox and Safari do not support it. Feature detection and fallback handling are required.

OPFS storage capacity limits?

OPFS shares the same-origin storage quota with Cache API and IndexedDB (typically 50%+ of available disk space). Query with navigator.storage.estimate().

Can Access Handle be used on the main thread?

Yes, but synchronous I/O blocks the main thread. Best practice: use Access Handle in a Worker to avoid blocking the UI.

How to handle large file writes?

Use WritableStream chunked writes to avoid loading the entire file into memory:

const writable = await handle.createWritable();
for await (const chunk of readableStream) {
  await writable.write(chunk);
}
await writable.close();

Summary

The File System Access API gives browsers native-app-level file read/write capabilities. showOpenFilePicker/showSaveFilePicker provide user-authorized file access, FileSystemFileHandle manages read/write permissions, OPFS provides authorization-free sandboxed storage, and Access Handle enables high-performance synchronous random read/write. This is the core infrastructure for building browser-side file processing tools.

Try these browser-local tools — no sign-up required →

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