はじめに

私たちを取り巻くWeb技術は、もはや社会的なインフラとしてめまぐるしく進化しています。HTMLやCSSはもちろんのこと、JavaScriptやライブラリ、フレームワークなど、それぞれがニーズにキャッチアップする形で、機能強化を繰り返しています。その中でも、Web技術の中核に位置するにもかかわらず、意外と見過ごされがちなのがHTMLの進化です。本連載は、このHTMLと関連するJavaScript APIにフォーカスして、その新機能を手軽に試していただこうというものです。理解も利用もたやすいHTMLなので、ライトな気持ちで「こんなことができるようになったのか」を感じていただきます。

[NOTE]サンプルについて
本記事の配布サンプルは、以下のURLから入手できます。新機能を試していくので、ブラウザは最新である方がよいでしょう。本記事のサンプルは、執筆時点で最新のChromeで動作することを確認しています。Firefox、Safariなどでは動作しないので注意してください。
https://github.com/wateryinhare62/mynavi_html/

連載第11回の目的

この回では、File System Access APIの続きとして、ディレクトリ内のファイル一覧の作成、ファイル/ディレクトリの作成/削除、オリジン固有のファイルシステムを安全に利用できるOPFSの使い方を紹介します。

  • 図1:完成サンプル

    図1:完成サンプル

ファイルとディレクトリの操作

File System Access APIでは、ファイルの読み書きに加え、ファイルやディレクトリの操作も可能です。ここでは、選択されたディレクトリの内容を一覧表示し、ファイルやディレクトリの作成/削除を行うファイルマネージャ的なサンプルを紹介します。

ファイルとディレクトリの一覧の作成

第10回では、ファイルに対してファイルハンドルを取得して、読み書きといった操作を行ってきました。加えてディレクトリに対してのハンドルが用意されていて、これがディレクトリハンドル(FileSystemDirectoryHandleオブジェクト)です。ディレクトリハンドルを使うと、ディレクトリ内のファイルやディレクトリのリストを取得でき、作成や削除などの操作も可能です。まずは、選択されたディレクトリ内のファイルとディレクトリの一覧を作成してみます(図2)。

  • 図2:ファイルとディレクトリの一覧の作成

    図2:ファイルとディレクトリの一覧の作成

リスト1は、ディレクトリを選択するボタンと、選択したディレクトリ名を表示するためのspan要素、ディレクトリ内のリストを格納するdiv要素を配置したHTMLです。

リスト1:mng_dir.html

<p><button id="select-button">ディレクトリを開く</button>&nbsp;
  <span id="dirname"></span>
</p>
<p id="result-message">ディレクトリを選択してください。</p>
<div id="dir-content"></div>

リスト2は、選択ボタンをクリックした際のイベントハンドラを含むJavaScriptコードです。

リスト2:mng_dir.js


(1)選択したディレクトリハンドルを保持
let directoryHandle = null;
const selectDirButton = document.getElementById('select-button');
const dirname = document.getElementById('dirname');
const resultMessage = document.getElementById('result-message');
const dirContent = document.getElementById('dir-content');
…略…
(2)ディレクトリハンドルを指定して一覧を作成する関数
async function showDirectoryContents(dirHandle) {
  try {
    (2)ディレクトリ名を表示し、一覧をクリアする
    dirname.textContent = dirHandle.name;
    dirContent.innerHTML = '';
    (3)項目を非同期イテレータで処理する
    for await (const entry of dirHandle.values()) {
      (4)エントリ名を取得し、種類がディレクトリなら'[DIR]'を追加
      const name = await entry.name;
      let typed_name = name;
      if (entry.kind === 'directory') {
        typed_name = '[DIR] ' + name;
      }
      (5)一覧に追加する
      dirContent.innerHTML += `<p><a href="#" id="entry-${name}">[X]</a>${typed_name}</p>`;
    }
  } catch (err) {
    console.error(err);
  }
}
(6)選択ボタンクリックに対応するイベントハンドラ
selectDirButton.addEventListener('click', async () => {
  …略…
  try {
    (7)ディレクトリを選択してディレクトリハンドルを取得、保存する
    directoryHandle = await window.showDirectoryPicker();
    (8)選択したディレクトリで一覧を表示
    showDirectoryContents(directoryHandle);
    resultMessage.textContent = 'ディレクトリの内容を表示しました。';
  } catch (err) {
    console.error(err);
  }
});

大きく、
(1)ディレクトリハンドルなどの宣言
(2)ディレクトリ内容の一覧表示の関数
(6)選択ボタンクリックに対応するイベントハンドラ
に分かれます。

(1)でディレクトリハンドルをグローバルに宣言するのは、操作の拠点として複数のイベントハンドラなどで共有するためです。同じように一覧表示も関数として独立させています。(2)の関数は、引数のディレクトリハンドルで一覧を作成します。以下の流れになります。

  • ディレクトリ名を表示用のspan要素に設定し、一覧のdiv要素を空にする((2))
  • ディレクトリ内の項目を非同期イテレータ(valuesメソッド)で処理する((3))
  • エントリ名をnameプロパティで取得し、種類(kindプロパティ)がディレクトリなら'[DIR]'を追加する((4))
  • 一覧のdiv要素に追加する。後述する削除操作のために、aタグで囲って「entry-エントリ名」形式のid属性を設定しておく((5))

(6)のイベントハンドラ関数の説明は不要と思いますが、(7)でshowDirectoryPickerメソッドでディレクトリを選択し保存して、(2)の関数を呼び出すのみです。
ボタンをクリックして適当なディレクトリを選択すると、本項冒頭の図2のように、ファイルとディレクトリの一覧がdiv要素に反映されることを確認できます。

ファイルとディレクトリの作成機能の追加

ディレクトリハンドルには、ファイルやディレクトリを作成するメソッドもあるので、それらを使った作成機能を追加してみます(図3)。

  • 図3:ファイル/ディレクトリの作成

    図3:ファイル/ディレクトリの作成

リスト3は、ファイル/ディレクトリ名の入力欄と、いずれかを作成するボタンを配置したHTMLです。

リスト3:mng_dir.html


<p><input type="text" id="name-input" placeholder="ファイル名かディレクトリ名を入力">
  <button id="create-file-button">ファイルを作成</button>&nbsp;
  <button id="create-dir-button">ディレクトリを作成</button>
</p>

リスト4は、作成ボタンをクリックした際のイベントハンドラを含むJavaScriptコードです。

リスト4:mng_dir.js


const nameInput = document.getElementById('name-input');
const createFileButton = document.getElementById('create-file-button');
const createDirButton = document.getElementById('create-dir-button');
(1)ファイル作成ボタンクリックのイベントハンドラ
createFileButton.addEventListener('click', async () => {
  …略…
  (2)入力された名前を取得し空でないことを確認する
  const filename = nameInput.value.trim();
  if (!filename) {
    resultMessage.textContent = 'ファイル名を入力してください。';
    return;
  }
  try {
    (3)指定された名前でファイルを作成してファイルハンドルを取得する
    const fileHandle = await directoryHandle.getFileHandle(filename, { create: true });
    (4)新たに一覧を作成する
    showDirectoryContents(directoryHandle);
    resultMessage.textContent = `ファイル ${filename} を作成しました。`;
  } catch (err) {
    console.error(err);
  }
});
(5)ディレクトリ作成ボタンクリックのイベントハンドラ
createDirButton.addEventListener('click', async () => {
  …略…
  (6)入力された名前を取得し空でないことを確認する
  const dirnameInput = nameInput.value.trim();
  …略…
  try {
    (7)指定された名前でディレクトリを作成してファイルハンドルを取得する
    const newDirHandle = await directoryHandle.getDirectoryHandle(dirnameInput, { create: true });
    (8)新たに一覧を作成する
    showDirectoryContents(directoryHandle);
    resultMessage.textContent = `ディレクトリ ${dirnameInput} を作成しました。`;
  } catch (err) {
    console.error(err);
  }
});

ファイルとディレクトリの作成手順はほぼ共通です。(1)がファイル作成、(5)がディレクトリ作成のイベントハンドラです。両者とも、以下の流れになります。

  • 入力されたファイル名/ディレクトリ名をテキスト入力ボックスから取得し、空でないことを確認する。空ならイベントハンドラを抜ける((2)(6))
  • 指定された名前でファイル/ディレクトリを作成してファイルハンドル/ディレクトリハンドルを取得する((3)(7))
  • ディレクトリ内の一覧を更新する((4)(8))

(3)(7)においては、それぞれgetFileHandleメソッド/getDirectoryHandleメソッドを使い、ファイル/ディレクトリを作成しています。これらのメソッドは、基本は引数で指定されたエントリに対するファイルハンドル/ディレクトリハンドルの取得です。しかしながら、createオプションをtrueに指定することで、存在しない場合には作成するという動作になります。ここでは戻り値は使っていませんが、たとえばディレクトリハンドルならば、作成したディレクトリに移動するといったことが可能です。
適当な名前を入力してファイル作成ボタンかディレクトリ作成ボタンをクリックすると、ファイルあるいはディレクトリが作成されます。本項冒頭の図3のように、一覧が作成物を含むように更新されることを確認できます。

ファイルとディレクトリの削除機能の追加

最後に、削除機能を追加します(図4)。

  • 図4:ファイル/ディレクトリの削除

    図4:ファイル/ディレクトリの削除

準備として、一覧の各エントリにid属性を付与しておきました。一覧クリックがエントリ上であれば、そのid属性からエントリ名を割り出し、削除を実行するという流れになります。リスト5は、一覧に設定するイベントハンドラを含むJavaScriptコードです。

リスト5:mng_dir.js


selectDirButton.addEventListener('click', async () => {
  …略…
  try {
    directoryHandle = await window.showDirectoryPicker();
    showDirectoryContents(directoryHandle);
    resultMessage.textContent = 'ディレクトリの内容を表示しました。';
    (1)一覧にイベントハンドラを設定する
    dirContent.addEventListener('click', async (event) => {
      event.preventDefault();
      (2)ターゲットからa要素を取得してid属性を確認する
      let aTag = event.target.closest('a');
      if (aTag.id.startsWith('entry-')) {
        (3)エントリ名を取得して確認メッセージを出す
        const name = aTag.id.substring(6);
        if (!confirm(`本当に ${name} を削除しますか?`)) {
          return;
        }
        (4)削除を実行する
        await directoryHandle.removeEntry(name, { recursive: true });
        showDirectoryContents(directoryHandle);
        resultMessage.textContent = `${name} を削除しました。`;
      }
    });
  }
  …略…
});

サンプルでは動的に一覧を生成しているので、イベントハンドラは(1)のように一覧のコンテナであるdiv要素に設定します。イベントハンドラでの処理手順は以下の通りです。

  • イベントターゲット(通常はa要素のコンテンツ)からclosestメソッドでa要素を取得して、id属性の先頭部が"entry-"に一致することを確認する((2))
  • id属性からsubstringメソッドでエントリ名を取得、確認メッセージを出す((3))
  • removeEntryメソッドで削除を実行する((4))

削除には、ファイル/ディレクトリの区別はなく、removeEntryメソッドを使います。recursiveオプションをtrueに指定すると、対象がディレクトリである場合に、下位のディレクトリも含めて再帰的に削除されます。
ファイル名およびディレクトリ名の左にある赤い[X]クリックすると、削除確認のポップアップが表示された後に、削除が実行されます。本項冒頭の図4のように、ファイルとディレクトリの一覧が削除を反映するように更新されることを確認できます。

OPFSによる安全なファイルシステムの利用

OPFSはOrigin Private File Systemの略で、オリジンにひも付けられた安全に利用できるファイルシステムです。オリジンとは、スキーム、ホスト、ポート番号から構成されるURLの一部です。たとえばhttps://naosan.jp:80がオリジンであり(ポート番号は省略される場合がある)、オリジンごとに隔離されたファイルシステムがOPFSというわけです。

OPFSのメリットとデメリット

OPFSはFile System APIの一部であり、セキュリティやパフォーマンスを向上させる目的の拡張です。File System APIでは、以下のような問題点がありました。

  • ユーザーが直接ファイルにアクセスできるため、セキュリティ上のリスクが高い
  • セキュリティリスクの回避のためにデータのチェックを厳密に行う必要があり、パフォーマンスに悪影響を与えることがある

OPFSはブラウザ内でオリジンごとの隔離されたプライベート領域を使用しており、ユーザーには見えない形でファイルを管理します。これにより、他のサイトなどからファイルにアクセスされるリスクを低減することが可能です。また、プライベートな領域に限定してアクセスを許可できるため、セキュリティチェックの範囲を限定できます。これにより、高速なファイル操作が可能になります。
なお、OPFSには以下のような制限があることも押さえておきましょう。何でもかんでもOPFSに収めるわけにはいかないということです。

  • ブラウザでサイトのデータをクリアすると、OPFSのデータも削除される
  • ブラウザのストレージクォータの上限までしか保存できない(ディスク容量の50~60%、ブラウザ依存)

OPFSを利用する

OPFSはあくまでもオリジンのためのファイルシステムで、OSのファイルシステムに自由自在にアクセスすることを目的としたものではありません。イメージ的にはローカルストレージのファイルシステム版というべきもので、オリジンに必要なデータをファイルの形で管理するものと思えばよいでしょう。 OPFSの利用は、ここまで解説してきたものと大きく変わるものではありません。FileSystemFileHandle、FileSystemDirectoryHandleを通じてOPFSのファイルシステムにアクセスできます。異なるのは、ディレクトリハンドルの取得をshowDirectoryPickerメソッドではなく、navigator.storageオブジェクトのgetDirectoryメソッドによって行う点です。
前節のサンプルを改変して、showDirectoryPickerメソッドの替わりにnavigator.storage.getDirectoryメソッドを呼び出すだけで、OPFSを使ったファイル管理が可能です(図5)。その他の部分には基本的に手を入れずに、同様の機能を実現できます。

  • 図5:OPFSによる安全なファイルシステムの利用

    図5:OPFSによる安全なファイルシステムの利用

リスト6:opfs.js


startButton.addEventListener('click', async () => {
  …略…
  try {
    directoryHandle = await navigator.storage.getDirectory();
    showDirectoryContents(directoryHandle);
    resultMessage.textContent = 'ディレクトリの内容を表示しました。';
    …略…
  }
  …略…
});

ボタンをクリックすると、まずは一覧表示は空になります。前節同様に、適当にファイルやディレクトリを作成すると、一覧がdiv要素に反映されることを確認できます。なお、OPFSはオリジンにひも付けられたファイルシステムであるので、ページを閉じても同じオリジンで再びアクセスすれば、最後の状態に復元されます。

まとめ

2回にわたって紹介してきたFile System APIはいかがでしたでしょうか。ブラウザでファイル操作が可能になるということで、Webアプリの可能性が大きく広がることをお伝えできたのではないかと思います。
次回は、ファイル操作に関連した内容として、Drag & Drop APIを紹介します。

WINGSプロジェクト 山内直(著) 山田 祥寛(監修)
有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティ(代表山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手がける。現在も執筆メンバーを募集中。興味のある方は、どしどし応募頂きたい。著書、記事多数。
RSS
X:@WingsPro_info(公式)@WingsPro_info/wings(メンバーリスト)
Facebook

<著者について>
WINGSプロジェクト所属のテクニカルライター。出版社を経てフリーランスとして独立。ライター、エディター、デベロッパー、講師業に従事。屋号は「たまデジ。」。