コンテンツへスキップ
HexaTransfer
ブログへ戻る
技術詳解

Browser Storage APIs for ファイル転送 Applications

IndexedDB、File System Access API、Cache APIをファイル転送アプリに活用する方法。ストレージ制限、パフォーマンス、ブラウザ互換性を解説します。

ファイル転送アプリのブラウザストレージは4つのAPIに明確に分かれています。IndexedDBはセッションやチャンクのメタデータ管理(トランザクション・非同期・型付き)、File System Access APIはユーザーのディスク上でギガバイト規模のファイルを直接読み書きする場合、Cache APIはHTTPレスポンスとアプリシェルのアセット、Storage Manager APIはクォータ管理と永続化ヒントに使います。Origin Private File System(OPFS)はサンドボックス化された高性能I/O用として独立しています。APIを正しく選ぶことが重要です。iOS SafariでのLimit上限が1GBからデスクトップChromeの「空きディスクの60%」まで幅があり、誤った選択は本番環境でQuotaExceededErrorを引き起こします。

各APIを適切な用途にマッピングする

行のような構造のデータ(アップロードセッション、完了チャンクインデックス、共有メタデータ、失効トークン)にはIndexedDBを使います。非同期・トランザクション・インデックス可能で、セッションをまたいで保持されます。

ファイル全体をRAMに読み込まずにバイトをディスクへ渡す必要がある場合(500MBを超える復号化ダウンロードに最適)はFile System Access APIを使います。Chrome・Edge・Operaはフルサポートしており、FirefoxとSafariはshowOpenFilePicker経由で読み取り専用のサブセットのみ実装しています。

HTTP Responseオブジェクト(JSバンドル、CSS、アイコン、キャッシュされたAPIレスポンスなど)にはCache APIが適しています。Service Worker内でのfetchインターセプトに最適化されています。

ユーザーから見えないサンドボックスの高速ストレージ(例:数ギガバイトの暗号化パス中の書き込みバッファ)が必要な場合はOPFS(File System Access APIのOrigin-Private系統)を使います。バイナリブロブの場合、IndexedDBより桁違いのスループットを発揮します。

扱いやすいIndexedDB

生のIndexedDBはイベントベースのAPIが煩雑で知られています。Jake Archibaldのidbパッケージ(gzip圧縮後1.5KB)かDexie.js(20KB、より豊富なクエリAPI)を使ってください。

import { openDB } from 'idb';
const db = await openDB('transfers', 2, {
  upgrade(db, oldVersion) {
    if (oldVersion < 1) {
      const sessions = db.createObjectStore('sessions', { keyPath: 'id' });
      sessions.createIndex('by_expiry', 'expiresAt');
    }
    if (oldVersion < 2) {
      db.createObjectStore('chunks', { keyPath: ['sessionId', 'index'] });
    }
  }
});
await db.put('sessions', { id: 'abc', fileName: 'report.pdf', expiresAt: Date.now() + 86400000 });

バージョンマイグレーションはupgradeコールバック内で実行されます。v1からv3へジャンプするユーザーが両方のステップを踏めるよう、必ずoldVersionでマイグレーションをガードしてください。

IndexedDBは構造化クローンを通じてBlobやFileの参照を含むほとんどのデータ形式を扱えます。タブをリロードした後もセッションレコード内にFileハンドルを保存して元のファイルのバイトを再読み込みできるため、再開可能なアップロードに最適です。

大容量ダウンロード向けFile System Access API

このAPIでブラウザの保存ダイアログへ書き込み可能なストリームを渡せます。

const handle = await window.showSaveFilePicker({
  suggestedName: 'decrypted-archive.zip',
  types: [{ description: 'Zip', accept: { 'application/zip': ['.zip'] } }]
});
const writable = await handle.createWritable();
await decryptionStream.pipeTo(writable);

バイトはJSヒープに乗ることなく直接ディスクへ流れます。ブラウザで10GBの復号化ファイルを保存する現実的な方法はこれだけです。

FirefoxとSafariのフォールバックにはStreamSaver.jsを使います。Service Workerを使ってダウンロードUIをトリガーするストリーミングレスポンスを合成する仕組みで、同じ操作感をわずかに多い部品数で実現します。

永続的なファイルハンドルにより、アプリはセッションをまたいでファイルを再オープンできます。showOpenFilePickerでユーザーが一度許可を与えれば、FileSystemFileHandleをIndexedDBに保存し、後でhandle.requestPermission()を呼び出すことで毎回のファイルプロンプトなしにアクセスを再取得できます。

スクラッチ領域としてのOPFS

Origin Private File Systemはオリジン別のサンドボックスストレージで、ファイルシステムのように動作しますがユーザーには見えません。

const root = await navigator.storage.getDirectory();
const fh = await root.getFileHandle('scratch.bin', { create: true });
const access = await fh.createSyncAccessHandle(); // workers only
access.write(buffer, { at: offset });
access.flush();
access.close();

createSyncAccessHandleはWebWorker(Service Workerを含む)内でのみ使用可能です。同期的で非常に高速であり、シーケンシャル書き込みのベンチマークではIndexedDBの3〜10倍を記録します。数百MBの暗号化出力をアップロード前にバッファするのに最適です。また、ユーザーのDownloadsフォルダを汚染せずに復号化された作業コピーをキャッシュできます。

Safari 17がsyncアクセスハンドル付きのOPFSを搭載し、Firefox 111が続きました。現在は3大ブラウザすべてがサポートしており、本番コードでの採用が現実的になっています。

ストレージクォータと対処法

すべてのAPIは同じオリジンのクォータプールを共有します。大まかな上限:

  • デスクトップChrome:空きディスクの60%
  • デスクトップFirefox:空きディスクの50%、デフォルトではオリジン当たり2GBが上限
  • デスクトップSafari:1GBで警告、ユーザー承認でディスクの最大〜20%まで拡張
  • iOS Safari:オリジン当たり1GB、未使用7日で積極的に削除
  • Chrome Android:空きディスクの10%、ディスク圧迫時に削除

実行時にクォータを確認する方法:

const { quota, usage } = await navigator.storage.estimate();
console.log(`Using ${(usage/1e9).toFixed(2)} GB of ${(quota/1e9).toFixed(2)} GB`);

重要なストアには永続化をリクエストします:

const persisted = await navigator.storage.persist();

ブラウザが永続ストレージを許可した場合はtrueを返し、プレッシャー下でも削除されません。Chromeはユーザーが十分に操作したサイトに自動的に許可を与えます。Firefoxはプロンプトを表示します。

アプリシェルとオフライン向けCache API

Cache APIはRequestResponseのペアを保存し、Service Worker内に最適です。

const cache = await caches.open('shell-v7');
await cache.addAll([
  '/', '/app.js', '/app.css', '/icons/192.png'
]);

インターセプト時に取得する方法:

self.addEventListener('fetch', (e) => {
  e.respondWith(caches.match(e.request).then(r => r ?? fetch(e.request)));
});

暗号化されたファイルバイトをCache APIに入れないでください。2GBのResponseオブジェクトはiOS Safariのクォータを一度に超え、後からrangeフェッチもできません。バイトはOPFSまたはFile System Access APIによるダイレクト書き込みで扱ってください。

削除とデータ損失を適切に処理する

永続化されていないストレージは削除されます。計画が必要です。iOS Safariはクォータに関係なく、未使用7日後に削除します。Chromeはディスクが実際に圧迫されたときのみ削除します。Firefoxはクォータプールが満杯になると最近使われていないオリジンを削除します。

2つの防御パターン:

  • 再作成できない状態(アップロードセッションID、部分的なチャンクオフセット)は、新しいページロード時にサーバーから再取得して続行できるリロード対応フォーマットで書き込む。
  • 長期的な状態にはnavigator.storage.persist()をリクエストし、ブラウザがプロンプトを表示したときにユーザーが確認できるUIを提供する。

損失が許されないデータはサーバーを正とする。ブラウザストレージは一晩で消えることもある高速キャッシュとして扱ってください。

ブラウザサポートの落とし穴

3つのトラップが繰り返し現れます。

  1. indexedDB.databases()の列挙はFirefoxでサポートされていません(「閉じたらCookieを削除」を選択したユーザーはイベントを発火させずにすべてのIndexedDBコンテンツを失います)。
  2. FileSystemFileHandle.queryPermission()はリロード後に動作が異なります。許可済みでも'prompt'を返すことがあります。常にrequestPermission()を防御的に呼び出してください。
  3. プライベート・シークレットモードはすべての3つのAPIに別個の小さいセッション限定クォータを与えます。通常ブラウジングで動くコードがプライベートウィンドウで即座にQuotaExceededErrorを引き起こすことがあります。

HexaTransferはセッション状態にIndexedDBを、ストリーミング暗号化中の暗号文バッファリングにOPFSを、対応ブラウザでの10GBの復号化ダウンロードにFile System Access APIを使っています。hexatransfer.com で試せます — 無料、アカウント不要、最大10GB。

アプリに適したスタックの選択

ほとんどの転送アプリに適した組み合わせは、メタデータ用のidbラッパー付きIndexedDB、暗号化・復号化スクラッチ領域用のOPFSsyncアクセスハンドル、Service Worker内のアプリシェル用Cache API、StreamSaverフォールバック付きFile System Access APIによる最終ダウンロード、オンボーディング時のnavigator.storage.persist()呼び出しです。現在出荷されているすべてのブラウザをカバーし、モバイルでのクォータ内に収まり、削除されても適切に復旧します。各APIをアダプター経由で扱うことで、OPFSに新メソッドが追加されたりSafariのクォータが引き上げられた際に、1ファイルを変更するだけで対応できます。

エンドツーエンド暗号化で大容量ファイルを安全に送信

エンドツーエンド暗号化で最大10GBのファイルを無料で転送。アカウント不要。ファイルはアップロード前にブラウザで暗号化されるため、他の誰にも読まれません。

ファイルを送信