はじめに

ここでは、実際に OPFS への非同期操作を行うサンプルプログラムを作成し、その動作の確認を行っていきたいと思います。

今回作成するサンプルプログラムは、機能限定の簡易な OPFS のファイラーを想定しています。

それでは、作成するサンプルプログラムの概要から順番に説明していきたいと思います。


サンプルプログラムの概要

  • OPFS 上のファイルを一覧表示する。(ただし、現状はディレクトリの対応は行わない)
  • OPFS へユーザーファイルをコピーできる。(ただし、現状コピー先は、ルート直下のみ対応)
  • OPFS 上のファイルを選択した場合、そのファイルの内容を表示する。(ただし、テキストや画像、PDFなどブラウザが表示できる形式のみ対応)
  • 選択したOPFS 上のファイルを削除することができる。

画面イメージ

┌─────────────────────────────────────────────────┐
│                                                 │
│  コピー   削除                                   │
│                                                 │
│ ┌─────────────┐ ┌─────────────────────────────┐ │
│ │  file1      │ │                             │ │
│ │  file2      │ │         (表示エリア)         │ │
│ │    :        │ │                             │ │
│ │             │ │                             │ │
│ │             │ │                             │ │
│ │             │ │                             │ │
│ │             │ │                             │ │
│ │             │ │                             │ │
│ └─────────────┘ └─────────────────────────────┘ │
│                                                 │
└─────────────────────────────────────────────────┘


サンプルプログラムの動作イメージの確認

まずは、サンプルプログラムの実際の動きを確認していただこうと思います。

下記のボタンを押下することで、ポップアップ画面が表示され、サンプルプログラムが起動します。

※ ポップアップ画面外をクリックすると、画面が閉じ、サンプルプログラムは終了します。


サンプルプログラムの概要(かんたん説明)

操作手順

手順①:本頁の「サンプルプログラム起動」を押す。

サンプルプログラムが起動される。

👉 起動時に、OPFS に保存されているファイルがある場合、一覧を表示します。

手順②:一覧からファイルを選択する。

選択されたファイルの中身が表示エリアに表示される。

👉 選択したファイルは、OPFS から読みだされる。

手順③:「コピー」ボタンを押す。

ユーザーファイル選択のダイアログボックスが表示される。

👉 選択したファイルは、OPFS のルートにコピーされ、一覧表示に追加される。

手順④:「削除」ボタンを押す。

選択されたファイルが、OPFS から削除される。

👉 削除されたファイルは、一覧表示と表示エリアから消える。


動作確認を実施する

このテストで確認したいこと

このテストでは、次のことを確認していきたいと思います:

  • OPFS 上のファイルの一覧を取得する仕組み
  • OPFS 上でのファイル作成
  • OPFS 上のファイルへの非同期操作による書き込み
  • OPFS 上のファイルの読み込み
  • OPFS 上ファイルの削除
  • ページをリロードしてもファイルが残ること

実際の操作手順(ここが重要)

  1. 手順:サンプルプログラムの起動

👉 OPFS のルート取得を確認します

  • navigator.storage.getDirectory()
  1. 手順:「コピー」により、ユーザーファイルからファイルを OPFS 上にコピーする。

👉 OPFS 上へのファイルの書き込みを確認します

  • FileSystemDirectoryHandle.getFileHandle()
  • FileSystemFileHandle.createWritable()
  • FileSystemWritableFileStream.write()
  • FileSystemWritableFileStream.close()
  1. 手順:OPFS 上にコピーしたファイルを一覧上でクリックして、内容を表示する。

👉 OPFS 上へのファイルの読み込みを確認します

  • FileSystemDirectoryHandle.getFileHandle()
  • FileSystemFileHandle.getFile()
  1. 手順:F5や再読み込みを実行した後、テストプログラムを再起動する。

👉 OPFS 上へのファイルの一覧取得と、コピー済みのファイルが消えずに残っていることを確認します

  • FileSystemDirectoryHandle.values()
  1. 手順:「削除」により、選択したファイルを OPFS 上から削除する。

👉 OPFS 上からのファイルの削除を確認します

  • FileSystemDirectoryHandle.removeEntry()

テスト結果まとめ

  • OPFS のアクセスは、まずディレクトリの先頭である root の FileSystemDirectoryHandle オブジェクトをブラウザから取得することが先決です。
  • 取得した FileSystemDirectoryHandle や FileSystemFileHandle の操作方法は、OPFS とユーザーが選択したファイルでほぼ共通です。
  • ファイル操作には、File オブジェクトが使用できるため、ファイル本体の読み込みを行わずに、ファイル名やファイルサイズといったメタ情報へのアクセスが可能です。
  • ファイルへの非同期書き込みは、書き込み用のストリームを作成して書き込みます。書き込みデータには、Blob や File オブジェクトを渡せる為、コードがシンプルです。一度 ArrayBuffer へ変換する処理も必要ありません。
  • 書き込み用のストリームは、既定では新しい内容を書き込むためのストリームが作成されます。そのため、既存ファイルの内容をそのまま保持したい場合は keepExistingData:true を指定する必要があります。詳細については、過去記事「データ保存技術/OPFS のファイル操作②」を参照してください。
  • ページをリロードしても OPFS 上のファイルは残る。
  • OPFS 上のファイルは明示的に削除しないか、ユーザーキャッシュを削除しない限り残り続けます。
  • Chrome DevTools には、現時点では OPFS の内容を直接参照する標準機能はありません。確認には、拡張機能の「OPFS Explorer」等の導入が必要です。

サンプルプログラムのコード紹介

下記に、今回使用したサンプルプログラムのコードを記します。

HTML( index.html )

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>簡易 OPFS ファイラー</title>
  <link rel="stylesheet" href="./style.css">
  <script defer type="module" src="./script.js"></script>
</head>
<body>
  <p>
    <input id='input' type="file">
    <button id='copyBtn'>コピー</button>
    <button id='deleteBtn'>削除</button>
  </p>
  <div id='allArea'>
    <div id='fileList'></div>
    <iframe id='viewArea'></iframe>
  </div>
  <div id='status'></div>
</body>
</html>

CSS( style.css )

body {
  padding: 1rem;
  background-color: rgb(253, 191, 76);
  overflow: hidden;
}
button {
  width: 200px;
}
.selected {
  background-color: rgb(167, 167, 167);
}
#input {
  display: none;
}
#allArea {
  display: flex;
}
#fileList {
  border: solid 1px;
  background-color: white;
  height: 350px;
  width: 30%;
  box-shadow: 5px 5px 10px rgba(0,0,0,0.3);
  border-radius: 5px;
  font-size: small;
  overflow: auto;
}
.fileItem {
  cursor: pointer;
  border-bottom: solid 1px;
  padding: 0.5rem;
} 
#viewArea {
  margin: 0px 0px 0px 10px;
  border: solid 1px;
  background-color: white;
  height: 350px;
  width: 70%;
  box-shadow: 5px 5px 10px rgba(0,0,0,0.3);
  border-radius: 5px;
  overflow: auto;
}
#status {
    font-size: 1.25rem;
    font-weight: bold;
    color: red;
    margin-top: 1rem;
    margin-bottom: 1rem;
}

メインスレッド( script.js )

const fileList = document.getElementById("fileList");
const viewArea = document.getElementById("viewArea");
const status = document.getElementById("status");
let currentObjectURL = null;

// OPFS のルートディレクトリを取得
const root = await navigator.storage.getDirectory();

// ファイル一覧項目の作成
function createFileItem(handle) {
  const fileItem = document.createElement("div");
  fileItem.id = handle.name;
  fileItem.textContent = `📄 ${handle.name}`;
  fileItem.classList.add("fileItem");

  // ファイル一覧項目クリック時の処理
  fileItem.addEventListener("click", async (e) => {
    const file = await handle.getFile(); // OPFS 上のファイルの取得

    // 取得したファイルを iframe に表示
    if (currentObjectURL) URL.revokeObjectURL(currentObjectURL);
    currentObjectURL = URL.createObjectURL(file);
    viewArea.src = currentObjectURL;

    // 既にハイライト表示のものがあれば解除し、選択したファイルをハイライト表示
    const selected = document.querySelector(".selected");
    if (selected) {
      selected.classList.remove("selected");
    }
    e.target.classList.add("selected");

    status.textContent = `選択しました: ${handle.name}`;
  });
  return fileItem; // 作成したファイル一覧項目を返す
}

// 現在の OPFS のファイル一覧を取得し、画面に表示
for await (const handle of root.values()) {
  const fileItem = createFileItem(handle);
  fileList.appendChild(fileItem);
}

// コピーボタン押下時に、ファイル選択ポップアップを表示
document.getElementById("copyBtn").addEventListener("click", (e) => {
  document.getElementById("input").click();
});

// ファイル選択ポップアップでファイルが選択された時の処理
document.getElementById("input").addEventListener("change", async (e) => {
  const file = e.target.files[0];
  e.target.value = ""; // 次回、同じファイルを選択できるようにする

  if (!file) return; // ファイルが選択されなかった場合は何もしない

  let fileHandle;

  try {
    fileHandle = await root.getFileHandle(file.name); // OPFS 上に同一名のファイルがあるか確認

    // 同一名のファイルが既に存在する場合、上書きするか確認する
    if (!confirm(`${file.name}」は既に存在します。\n上書きしてもよろしいですか?`)) return;
  }
  catch (err) {
    if (err.name === "NotFoundError") {
      // 同一名のファイルが無い場合、新規作成する
      fileHandle = await root.getFileHandle(file.name, { create: true });

      const fileItem = createFileItem(fileHandle);
      fileList.appendChild(fileItem);
    } else {
      throw err;
    }
  }
  
  // OPFS ファイルに書き込み
  const writable = await fileHandle.createWritable(); // 書き込み用のストリームを作成
  await writable.write(file); // ユーザーファイルの内容を OPFS のファイルに書き込む
  await writable.close(); // 書き込み用のストリームを閉じる

  status.textContent = `ファイルをコピーしました: ${fileHandle.name}`;
});

// 削除ボタン押下時に、選択中のファイルを削除
document.getElementById("deleteBtn").addEventListener("click", async (e) => {
  const selected = document.querySelector(".selected");
  if (!selected) return; // 選択中のファイルが無い場合は何もしない

  await root.removeEntry(selected.id); // ファイルの削除

  selected.remove();

  // 不要になった Object URL を解放
  if (currentObjectURL) {
    URL.revokeObjectURL(currentObjectURL);
    currentObjectURL = null;
  }
  viewArea.src = "";

  status.textContent = `削除しました: ${selected.id}`;
});

URL.createObjectURL() で生成した Object URL はブラウザ内部で管理されるため、不要になったら URL.revokeObjectURL() を呼び出して解放することをおすすめします。