Featured image of post File System Access API:強力なローカルファイル操作 Featured image of post File System Access API:強力なローカルファイル操作

File System Access API:強力なローカルファイル操作

File System Access APIの包括的ガイド:showOpenFilePicker、showSaveFilePicker、showDirectoryPicker、ストリーミング書き込み、権限モデル、IDE風アプリパターンを解説。

はじめに

長年にわたり、Webアプリケーションは<input type="file">要素による読み取り専用のファイルアクセスに制限され、元のファイルに変更を保存する信頼できる方法がありませんでした。File System Access APIはこの状況を根本から変えます。ユーザーの許可を得た上で、ローカルファイルシステム上のファイルやディレクトリを直接読み書き・管理できるようになります。これにより、テキストエディタ、画像エディタ、IDE、生産性ツールなど、ネイティブアプリに迫る操作性を持つWebアプリケーションの構築が可能になります。


showOpenFilePickerでファイルを開く

window.showOpenFilePicker()メソッドは、FileSystemFileHandleオブジェクトの配列に解決されるPromiseを返します。複数選択、ファイルタイプフィルター、MIMEタイプの制限などをオプションで指定できます:

const [fileHandle] = await window.showOpenFilePicker({
  types: [{
    description: 'Markdown Files',
    accept: { 'text/markdown': ['.md'] }
  }],
  multiple: false
});

const file = await fileHandle.getFile();
const content = await file.text();
document.querySelector('#editor').value = content;

FileSystemFileHandleインターフェースは、getFile()(読み取り)とcreateWritable()(書き込み)の2つの重要なメソッドを提供します。読み取りはデータタイプに応じてfile.text()file.arrayBuffer()file.stream()を使い分けます。エラー処理も重要です。ユーザーのキャンセルはAbortError、権限の取り消しはNotAllowedErrorをスローします。


showSaveFilePickerで保存する

保存にはwindow.showSaveFilePicker()を使用し、ユーザーが選択した保存先のFileSystemFileHandleを取得します。書き込みは、書き込み可能なストリームを作成し、内容を書き込み、ストリームを閉じるという3ステップです:

const fileHandle = await window.showSaveFilePicker({
  suggestedName: 'document.md',
  types: [{
    description: 'Markdown Files',
    accept: { 'text/markdown': ['.md'] }
  }]
});

const writable = await fileHandle.createWritable();
await writable.write(editorContent);
await writable.close();

大容量ファイルの場合は、WriteParamsオブジェクトを使用したストリーミング書き込みにより、ファイル全体をメモリに読み込むことなくチャンク単位で書き込めます:

const writable = await fileHandle.createWritable();
const encoder = new TextEncoder();
const chunkSize = 1024 * 1024; // 1MB単位

for (let offset = 0; offset < content.length; offset += chunkSize) {
  const chunk = content.slice(offset, offset + chunkSize);
  await writable.write({ type: 'write', data: encoder.encode(chunk), position: offset });
}
await writable.close();

WriteParamsオブジェクトはtype: 'truncate'(ファイルサイズ変更)とtype: 'seek'(書き込み位置の移動)もサポートしており、ファイルコンテンツを精密に制御できます。


ディレクトリ操作

window.showDirectoryPicker()メソッドはFileSystemDirectoryHandleを返し、ディレクトリ構造の再帰的走査や変更を可能にします:

const dirHandle = await window.showDirectoryPicker();

async function traverseDirectory(dirHandle, path = '') {
  for await (const [name, handle] of dirHandle.entries()) {
    const fullPath = `${path}/${name}`;
    if (handle.kind === 'directory') {
      console.log(`📁 ${fullPath}`);
      await traverseDirectory(handle, fullPath);
    } else {
      console.log(`📄 ${fullPath}`);
    }
  }
}

await traverseDirectory(dirHandle);

新しいファイルやディレクトリの作成も直感的です:

const newFile = await dirHandle.getFileHandle('notes.md', { create: true });
const newDir = await dirHandle.getDirectoryHandle('images', { create: true });
await dirHandle.removeEntry('temp.log', { recursive: true });

権限モデル

File System Access APIは一時的アクティベーションを必要とします。ピッカーメソッドはクリックやキー押下などのユーザージェスチャに応答して呼び出されなければなりません。ファイルおよびディレクトリハンドルの権限は、ページがリロードされるかハンドルがガベージコレクトされるまで保持されます。より長期的なアクセスには、明示的に権限を確認・要求できます:

async function requestWritePermission(fileHandle) {
  const options = { mode: 'readwrite' };
  if ((await fileHandle.queryPermission(options)) === 'granted') {
    return true;
  }
  return (await fileHandle.requestPermission(options)) === 'granted';
}

ハンドルはシリアライズしてIndexedDBに保存でき、セッションを超えた永続的なアクセスが可能です。これはIDE的なアプリケーションが以前に開いたプロジェクトを記憶する仕組みとして利用されます。プライベートブラウジングモードでは保存済みハンドルがクリアされ、クロスオリジンiframeはAPIにアクセスできない点に注意してください。


ブラウザサポートとフォールバック

機能ChromeEdgeFirefoxSafari
File System Access API86+86+
File Handling API102+102+
Origin Private File System86+86+

機能検出とフォールバックは必須です:

if ('showOpenFilePicker' in window) {
  // File System Access APIを使用
} else {
  // 代替として<input type="file">を使用
  const input = document.createElement('input');
  input.type = 'file';
  input.click();
}

非対応ブラウザでの保存には、Blob URLを使用した<a download>で代替します。File Handling API(Chrome 102+)を使用すると、Web App Manifestを通じてアプリを特定のファイルタイプのハンドラーとして登録でき、OSから直接ファイルを開けるようになります。


高度なユースケース

File System Access APIは、これまでネイティブアプリケーションに限られていたパターンを実現します:

  • IDE風アプリケーション:プロジェクトディレクトリを開き、個別ファイルの読み書き、新規作成、リネーム、削除。
  • マークダウンエディタ+ライブプレビュー:.mdファイルを開き、リアルタイムプレビュー付きで編集し、元ファイルに保存。
  • 画像エディタのSave/Save AsshowSaveFilePickerでSave As、createWritable()でその場保存。
  • 一括ファイル処理:ディレクトリを開き、ファイルを反復処理、変換を適用して結果を保存 — ダウンロードやアップロードは不要。

ユーザージェスチャ不要のサンドボックスストレージが必要な場合は、navigator.storage.getDirectory()でOrigin Private File Systemを使用します。これはオリジン単位の仮想ファイルシステムで、キャッシュやオフラインデータに適しています。


パフォーマンス考慮事項

大容量ファイルを扱う際は、常にストリーミング手法を優先しましょう:

  • 大容量ファイルの読み取りはfile.text()file.arrayBuffer()ではなくfile.stream()を使用。
  • メモリ負荷を避けるため、チャンク(例:1MB)単位で書き込み。
  • 数千ファイルを含むディレクトリは、一括収集ではなくentries()イテレータを使用。
  • 自動保存はデバウンス処理し、書き込みストリームを開いたまま定期的に書き込むが、完了時にのみクローズ。
  • Origin Private File System(navigator.storage.getDirectory())は権限プロンプト不要で、アプリ内部データに最適。

まとめ

File System Access APIは、Webアプリケーションを読み取り専用のコンテンツコンシューマからフル機能のクリエイティブツールへと進化させます。現在はChromium系ブラウザに限定されていますが、このAPIが埋める機能ギャップは計り知れません。常に機能検出を行い、従来の<input type="file"><a download>パターンによるフォールバックを提供しましょう。ネイティブファイル機能を必要とする生産性アプリ、クリエイティブツール、開発者ツールにとって、このAPIはElectronやその他のネイティブラッパーを不要にします。