Origin Private File SystemとFile System Access APIの内部
ブラウザでネイティブ並みのファイルI/Oが扱える理由が分かる。OPFS の隔離ツリーと同期アクセスハンドルの高速書き込み、ユーザー可視 FS への権限モデルを内部動作から正確に整理します。
- 1.OPFS はオリジンごとに隔離された不可視のファイルツリーで、ユーザーのファイルシステムとは別空間。ディレクトリ/ファイルハンドルでアクセスする。
- 2.ワーカー上の createSyncAccessHandle は同期 read/write/flush を提供し、コピーや構造化複製を介さない最速の I/O 経路になる。
- 3.ユーザー可視 FS への showOpenFilePicker 等はユーザー操作(一時的活性化)を起点に権限を得る方式で、OPFS とは権限モデルがまったく異なる。
2つの「ファイルシステム」を混同しない
File System API には、性質のまったく異なる2つの世界が同居しています。1つは Origin Private File System(OPFS)、もう1つは showOpenFilePicker などによるユーザー可視ファイルシステムへのアクセスです。両者は同じハンドル型(FileSystemDirectoryHandle / FileSystemFileHandle)を共有しますが、保存場所も権限モデルも性能特性も別物です。
| 観点 | OPFS | ユーザー可視 FS(File System Access) |
|---|---|---|
| 保存場所 | ブラウザ管理の隔離領域(ユーザーには見えない) | 実際のディスク上のユーザーファイル |
| 入口 | navigator.storage.getDirectory() | showOpenFilePicker / showSaveFilePicker / showDirectoryPicker |
| 権限 | オリジンに暗黙付与(プロンプト不要) | ユーザー操作を起点に明示許可が必要 |
| 同期 I/O | ワーカーで可(SyncAccessHandle) | 不可 |
| 対応状況 | 主要ブラウザで広く実装 | Chromium 中心、Safari は OPFS のみ |
混乱の元は「同じ API 名前空間に乗っている」ことです。実装上は、OPFS はブラウザのストレージ容量管理(クォータ)の配下にある内部ストレージで、ユーザー可視 FS は OS のファイルダイアログとパーミッションを介した外部リソースです。以降、この区別を軸に内部を見ていきます。
OPFS:オリジンに隔離されたファイルツリー
OPFS は、各オリジンに割り当てられた専用のルートディレクトリを起点とする階層ファイルツリーです。入口は1つだけです。
// OPFS のルート(FileSystemDirectoryHandle)を取得
const root = await navigator.storage.getDirectory();
// サブディレクトリとファイルを作る(create: true で無ければ作成)
const dir = await root.getDirectoryHandle("logs", { create: true });
const file = await dir.getFileHandle("2026-06.bin", { create: true });
ここで返るルートは、localStorage のキー空間や IndexedDB のオブジェクトストアと同じく、オリジン単位で完全に分離されています。別オリジンの OPFS は見えず、同一オリジンポリシーがそのまま境界になります。ユーザーは OS のファイルマネージャからこのツリーを直接たどることはできず、ブラウザがバックエンド(実体はディスク上のどこか、ただしファイル名や構造はブラウザの内部表現)として管理します。
OPFS が他のストレージと根本的に違うのは、バイト列としてのファイルを直接扱える点です。IndexedDB が構造化複製を介したレコードの出し入れであるのに対し、OPFS は「ファイルの任意オフセットに任意バイトを書く」という低レベル操作を提供します。これが SQLite の WASM ビルドや、独自フォーマットの大容量データを扱うアプリで OPFS が選ばれる理由です。
OPFS はユーザーのファイルを触りません。あくまでブラウザが各オリジンに割り当てた砂場(サンドボックス)であり、漏れ出す先がないため許可ダイアログ無しで使えます。プライバシー上の判断材料がないので、権限は「オリジンであること」だけで足ります。
SyncAccessHandle:最速 I/O 経路の正体
OPFS の性能上の主役が createSyncAccessHandle() です。これはWeb ワーカー上でのみ取得でき、メインスレッドでは InvalidStateError になります。返ってくる FileSystemSyncAccessHandle は、名前のとおり同期のメソッド群を持ちます。
// 必ずワーカー(DedicatedWorker 等)内で実行する
const root = await navigator.storage.getDirectory();
const fh = await root.getFileHandle("db.sqlite", { create: true });
const access = await fh.createSyncAccessHandle();
const buf = new Uint8Array([0x53, 0x51, 0x4c]);
access.write(buf, { at: 0 }); // 同期。Promise ではなく書き込んだバイト数を返す
const out = new Uint8Array(3);
access.read(out, { at: 0 }); // 同期。読み込んだバイト数を返す
access.flush(); // ディスクへ確実に反映
access.truncate(0); // サイズ変更
access.close(); // ハンドルを解放(同一ファイルの排他ロックを解く)
なぜこれが速いのか。理由は3つあります。
- 同期呼び出し:
read/writeがイベントループのタスクを介さず、その場で値を返す。1操作ごとに イベントループ のターンを跨ぐ非同期 API のオーバーヘッドが無い。 - コピーの最小化:渡す
ArrayBuffer/TypedArrayのバイト列を、構造化複製や JSON 変換を挟まずほぼそのまま I/O に流せる。バッファの中身については TypedArray とメモリ の理解がそのまま効く。 - オフセット指定 I/O:
atでファイル内の任意位置を直接読み書きできるため、ファイル全体を読み直さずに部分更新できる。
同期 API がワーカー限定なのは設計上の必然です。同期 I/O はその呼び出しの間スレッドをブロックするため、UI を動かすメインスレッドで許すとフリーズを招きます。ワーカーは専用スレッドなので、ブロックしても UI に波及しません。
createSyncAccessHandle() は対象ファイルに排他ロックを取ります。同じファイルに対して2つ目のハンドルを開こうとすると NoModificationAllowedError で失敗します。処理が終わったら必ず close() を呼んでロックを解放してください。ワーカーを使い捨てにする設計でも、close 漏れは次回オープンの失敗として跳ね返ります。
非同期の書き込みストリーム(メインスレッド向け)
ワーカーを立てずメインスレッドで書きたい場合は、createWritable() が返す FileSystemWritableFileStream を使います。これは Streams API の WritableStream を継承した非同期ストリームです。
const fh = await root.getFileHandle("note.txt", { create: true });
const w = await fh.createWritable();
await w.write("hello"); // 末尾追記ではなく現在位置に書く
await w.write({ type: "seek", position: 0 });
await w.write("HELLO");
await w.close(); // ここで初めて実ファイルへ反映される
重要なのは書き込みのアトミック性です。createWritable() は既定で**一時ファイル(スワップファイル)**に書き、close() の時点で元ファイルへ置き換えます。つまり close するまで元ファイルは変わらず、途中でクラッシュしても破損しません。逆に言えば、{ keepExistingData: false } の既定では一時ファイルが空から始まるため、部分更新したいときは keepExistingData: true を渡して既存内容を引き継ぐ必要があります。
| I/O 経路 | 実行コンテキスト | 同期性 | アトミック性 |
|---|---|---|---|
| SyncAccessHandle | ワーカーのみ | 同期 | なし(直接書き込み・即時反映は flush 次第) |
| WritableFileStream | メイン/ワーカー両方 | 非同期 | あり(close で置換) |
用途で選びます。データベースエンジンのような高頻度ランダム I/O は SyncAccessHandle、ファイルを丸ごと書き出す保存処理は WritableStream が素直です。
ユーザー可視 FS の権限モデル
もう一方の世界、showOpenFilePicker などはユーザーの実ファイルを触るため、権限の扱いが厳密です。核になる概念は一時的活性化(transient activation)です。これらのピッカーは、クリックやキー入力といったユーザー操作の直後でなければ呼び出せません。load イベントや setTimeout から勝手に開くことはできず、SecurityError になります。
button.addEventListener("click", async () => {
// ユーザー操作起点なので呼べる
const [handle] = await window.showOpenFilePicker({ multiple: false });
const file = await handle.getFile(); // 読み取りは許可済み
const text = await file.text();
// 書き戻すには writable 権限が要る
const perm = await handle.queryPermission({ mode: "readwrite" });
if (perm !== "granted") {
await handle.requestPermission({ mode: "readwrite" }); // 再びプロンプト
}
});
権限は granted / denied / prompt の3状態を取り、read と readwrite で別々に管理されます。読み取り用に開いたハンドルへ書き戻すには、改めて requestPermission({ mode: "readwrite" }) を呼び、ユーザーの再許可が要ります。さらにこの権限はそのページのセッション内で有効なのが基本で、ページを再読み込みすると再許可が必要になります(ハンドル自体は 構造化複製可能 なので IndexedDB に保存して持ち越せますが、権限状態は別途 requestPermission で復活させます)。
ユーザー可視 FS では、ブラウザがシステム上重要なディレクトリ(OS のシステムフォルダ、ブラウザ本体のプロファイル領域など)への保存・選択をブロックします。ユーザーが選んだとしても拒否される場合があり、これは仕様上の安全策です。任意パスへ自由に書ける API ではない点を前提に設計してください。
ハンドルが同じ型でも、出どころで権限挙動が決まります。navigator.storage.getDirectory() 由来=OPFS(プロンプト無し・同期 I/O 可)。showOpenFilePicker 等の由来=ユーザー可視 FS(プロンプト必須・同期 I/O 不可)。queryPermission が常に granted を返すなら前者、と覚えると実装時に迷いません。
まとめ
File System API の要諦は、OPFS とユーザー可視 FS という別系統を1つの API 名前空間が束ねている、という構造の把握です。OPFS はオリジンに隔離されたサンドボックスで、権限プロンプト無しに使え、ワーカー上の createSyncAccessHandle で同期・低コピーの最速 I/O を提供します。これがブラウザ内 SQLite のような重量級データ層を可能にしています。一方ユーザー可視 FS は、一時的活性化を起点に read/readwrite を個別に許可する厳格なモデルで、実ファイルを安全に扱うための制約を伴います。どちらを使うかは「ブラウザ内で完結する高速ストレージが欲しいのか、ユーザーのファイルを編集したいのか」で決まり、その判断軸さえ持てば残りの API は素直に追えます。容量管理や退避の挙動は IndexedDB の内部 と共通なので、保存先全体の設計としてあわせて押さえてください。
Web/フロントエンド Article
Origin Private File SystemとFile System Access APIの内部を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
ブラウザ
比較で見る軸
難易度: advanced / カテゴリ: Web/フロントエンド / タグ数: 5
導入後に効く点
ワーカー上の createSyncAccessHandle は同期 read/write/flush を提供し、コピーや構造化複製を介さない最速の I/O 経路になる。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- Web/フロントエンド
- タグ数
- 5
判断チェックリスト
- 自社の用途が「ブラウザ / OPFS」に近いか確認する。
- 強みである「OPFS はオリジンごとに隔離された不可視のファイルツリーで、ユーザーのファイルシステムとは別空間。ディレクトリ/ファイルハンドルでアクセスする。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。