File System Access API in Practice: Browser-Side Local File Read/Write Architecture
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}`);
}
}
Recursive File Search
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 →