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: '画像ファイル',
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画像',
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();
const text = await file.text();
const buffer = await file.arrayBuffer();
const stream = file.stream();
ファイルへの書き込み
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(`ファイル: ${name}, サイズ: ${file.size}`);
} else if (handle.kind === 'directory') {
console.log(`ディレクトリ: ${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);
}
}
}
for await (const pdf of findFiles(dirHandle, /\.pdf$/)) {
console.log(pdf.name);
}
Origin Private File System (OPFS)
OPFSはブラウザが各オリジンに提供するプライベートサンドボックスファイルシステムで、ユーザー許可不要:
const opfsRoot = await navigator.storage.getDirectory();
const fileHandle = await opfsRoot.getFileHandle('work-data.bin', { create: true });
const subDir = await opfsRoot.getDirectoryHandle('cache', { create: true });
await opfsRoot.removeEntry('work-data.bin');
OPFS vs IndexedDB
| 特性 | IndexedDB | OPFS |
|---|---|---|
| ストレージ型 | 構造化データ | ファイル(バイナリ) |
| 読み書き方式 | トランザクション + シリアライズ | 直接ファイルI/O |
| 大ファイル性能 | 低い(シリアライズオーバーヘッド) | 優秀 |
| ランダムアクセス | ❌ | ✅(Access Handle) |
| 容量 | 同一オリジン制限 | 同一オリジン制限 |
| Workerサポート | ✅ | ✅ |
Access Handle:高性能ランダム読み書き
OPFSファイルはcreateSyncAccessHandle()をサポートし、同期・位置指定のファイル読み書きを提供—ネイティブファイルシステムに近いパフォーマンス:
const fileHandle = await opfsRoot.getFileHandle('large-data.bin', { create: true });
const accessHandle = await fileHandle.createSyncAccessHandle();
const writeBuffer = new Uint8Array([1, 2, 3, 4, 5]);
accessHandle.write(writeBuffer, { at: 0 });
const readBuffer = new Uint8Array(5);
accessHandle.read(readBuffer, { at: 0 });
const fileSize = accessHandle.getSize();
accessHandle.flush();
accessHandle.close();
重要な利点:Access Handleは同期で、Worker内でasync/awaitなしで使用可能—WritableStreamより10-100倍高速。
実践:PDF結合のファイル読み書きアーキテクチャ
ToolsKuのPDF結合はFile System Access APIで「開く→処理→保存」の完全なワークフローを実現:
async function mergePdfWorkflow() {
const handles = await window.showOpenFilePicker({
types: [{ accept: { 'application/pdf': ['.pdf'] } }],
multiple: true,
});
const buffers: ArrayBuffer[] = [];
for (const handle of handles) {
const file = await handle.getFile();
buffers.push(await file.arrayBuffer());
}
const mergedPdf = await mergePdfs(buffers);
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はメインスレッドで使えるか?
可能ですが、同期I/Oはメインスレッドをブロックします。ベストプラクティス:Worker内でAccess Handleを使用し、UIブロックを回避してください。
大きなファイルの書き込み処理?
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が高性能な同期ランダム読み書きを実現します。これはブラウザ側ファイル処理ツール構築のコアインフラです。
ブラウザローカルツールを無料で試す →