OPFS(Origin Private File System)についてまとめる
はじめに
SQLite Wasmの内部でOPFS(Origin Private File System)を使用していることを知りました。
OPFSについて詳しく理解するために本記事を執筆しています。
OPFS(Origin Private File System)とは
OPFS(Origin Private File System)とは、ブラウザ内でOriginごとの隔離されたプライベートな領域を提供するファイルシステムのことです。
これはFile System Access APIの一部として提供されており、従来のAPIで抱えていたセキュリティやパフォーマンスの課題を解決するために作られました。
File System Access APIとの違い
File System Access APIとは異なり、OPFSはセキュリティ面とパフォーマンス面が優れています。
セキュリティ
OPFSはセキュリティ面で優秀です。
File System Access APIはユーザーが直接閲覧できるファイルにアクセスできるため、セキュリティ上のリスクが高い状況でした。
一方、OPFSはブラウザ内でOriginごとの隔離されたプライベート領域を使用しており、ユーザーには見えない形でファイルを管理します。
これにより、他のサイトなどからファイルにアクセスされるリスクを低減することが可能です。
パフォーマンス
OPFSはパフォーマンス面でも優れています。
File System Access APIではユーザーが直接閲覧できるファイルにアクセスするため、悪意のあるデータのチェックをより厳密に行う必要があり、パフォーマンスに影響を与えることがあります。
一方、OPFSはOriginごとの隔離されたプライベートな領域に限定してアクセスを許可するため、セキュリティチェックの範囲が限定されます。
これにより、高速なファイル操作が実現されています。
OPFSの注意点
- サイトのストレージをクリアすると、データが削除される。
- ブラウザのストレージ容量制限の対象であるため、保存できるデータ量に制限がある。
- SafariやFirefoxでは、
FileSystemFileHandle.createWritable()
メソッドがサポートされていない。
OPFSの使い方
実際にOPFSを使ってみました。
OPFSにアクセスするためには、StorageManager.getDirectory()
メソッドを呼び出す必要があります。
このメソッドは、OPFSのルートを表すFileSystemDirectoryHandle
オブジェクトを返します。
const root = await navigator.storage.getDirectory();
ディレクトリ
最初にディレクトリの操作についてまとめます。
取得
ディレクトリの取得方法です。
指定したディレクトリが存在しない場合、NotFoundError
が発生します。
// ルートを取得
const root = await navigator.storage.getDirectory();
// examplesディレクトリを取得
const dirHandle = await root.getDirectoryHandle('examples');
作成
ディレクトリの作成方法です。
FileSystemDirectoryHandle.getDirectoryHandle()
メソッドにcreate
オプションを指定することで、ディレクトリが存在しない場合に新たなディレクトリが作成されます。
既にディレクトリが存在する場合は、そのディレクトリが返却されます。
// ルートを取得
const root = await navigator.storage.getDirectory();
// examplesディレクトリを作成
const dirHandle = await root.getDirectoryHandle('examples', { create: true });
// examplesディレクトリの中にsub-examplesディレクトリを作成
const subDirHandle = await dirHandle.getDirectoryHandle('sub-examples', { create: true });
削除
ディレクトリの削除方法です。
recursive
にtrue
を指定することで、ディレクトリ内のファイルおよびディレクトリが再帰的に削除されます。
ディレクトリが空ではない場合、recursive
にtrue
を指定しないとInvalidModificationError
が発生するため注意が必要です。
// ルートを取得
const root = await navigator.storage.getDirectory();
// examplesディレクトリを再帰的に削除する
await root.removeEntry('examples', { recursive: true });
ファイル
続いてファイルの操作についてまとめます。
取得
ファイルの取得方法です。
指定したファイルが存在しない場合、NotFoundError
が発生します。
ファイルの中身を読み込む処理も記載しています。
// ルートを取得
const root = await navigator.storage.getDirectory();
// example.txtファイルを取得
const fileHandle = await root.getFileHandle("example.txt");
// example.txtファイルの中身を読み込む(Fileオブジェクトが返却される)
const file = await fileHandle.getFile();
const content = await file.text();
// 出力
console.log(content)
作成
ファイルの作成方法です。
FileSystemDirectoryHandle.getFileHandle()
メソッドにcreate
オプションを指定することで、ファイルが存在しない場合に新たなファイルが作成されます。
既にファイルが存在する場合は、そのファイルが返却されます。
// ルートを取得
const root = await navigator.storage.getDirectory();
// example.txtファイルを作成
const fileHandle = await root.getFileHandle("example.txt", { create: true });
書き込み(非同期)
ファイルを非同期で書き込む方法です。
FileSystemFileHandle.createWritable()
メソッドで書き込み可能なストリームを取得し、ファイルにデータを書き込みます。
ただし注意点として、FileSystemFileHandle.createWritable()
メソッドがSafari、Firefoxではサポートされていません。
// ルートを取得
const root = await navigator.storage.getDirectory();
// example.txtファイルを作成
const fileHandle = await root.getFileHandle("example.txt", { create: true });
// ファイルの書き込みを行うストリームを作成
const writable = await fileHandle.createWritable();
// ファイルにHello, world!を書き込む
await writable.write("Hello, world!");
// ストリームを閉じる
await writable.close();
書き込み(同期)
ファイルを同期的に書き込む方法です。
こちらはWeb Workerを使います。
Web Workerはメインスレッドをブロックしないため、同期的にOPFSを扱うことが可能です。
SafariやFirefoxでは非同期でファイルにデータを書き込めないため、基本的にWeb Workerを使うことになると思います。
// ルートを取得
const root = await navigator.storage.getDirectory();
// example.txtファイルを作成
const fileHandle = await root.getFileHandle("example.txt", { create: true });
// 同期的にファイルへアクセスするためのオブジェクトを作成
const accessHandle = await fileHandle.createSyncAccessHandle();
// テキストをバイナリにエンコードするためのエンコーダーとデコーダーを初期化
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
// 書き込むデータをエンコードする
const content = textEncoder.encode("Hello, world!");
// ファイルの先頭にエンコードしたデータを書き込む
accessHandle.write(content, { at: 0 });
// 書き込み操作をファイルに反映させる
accessHandle.flush();
// リソースを開放
accessHandle.close();
削除
ファイルの削除方法です。
指定したファイルが存在しない場合、NotFoundError
が発生します。
// ルートを取得
const root = await navigator.storage.getDirectory();
// example.txtを削除
await root.removeEntry("example.txt");
OPFSで作成したディレクトリとファイルをブラウザで確認する
拡張機能のOPFS Explorerを使用することで、OPFSで作成したディレクトリとファイルを確認することができます。
拡張機能を有効にした後、Chrome DevToolsのタブに「OPFS Explorer」が追加されていると思います。
試しに、ZennやQiita、XなどでOPFS Explorerを使ってOPFSの使用状況を調べてみましたが、特に使われていないことが分かりました。
主要ブラウザのSafari、Firefoxが非同期の書き込みに対応できていないため、少々使いづらいのかもしれません。
さいごに
今回はOPFSについてまとめてみました。
まずは下書きなどの一時的なデータの保存にOPFSを使用してみたいと思います。
Discussion
ファイルを保存をする為の web worker を作ると捗るやつですね。(zipでbackup / restore するところまで作るととてもよいです。
いつかやりたいです!
クラウドストレージと違って費用もかからないので気楽に使えますね。
ただし、 予め
navigator.storage.persist()
を呼んで 永続化ストレージにしておかないと ストレージ圧で削除される可能性があることに注意が必要です。勉強になります...!
あと永続化の観点で少々考えたのですが、
navigator.storage.persist()
で永続化ストレージを設定した後に大容量のファイルをOPFSで保存し続けることで、ユーザーのストレージ容量を圧迫させる悪質なプログラムを作成できる気がしました。以下の記事によると、Chromeでは総ディスクサイズの60%までのデータを格納可能みたいです。
悪さする人が出てくると危険ですね...。