【JavaScript】File System Access API – ディレクトリを開く方法 showDirectoryPicker() 編

File System Access API にはファイルを開く showOpenFilePicker() メソッド、ファイル保存ダイアログを表示する showSaveFilePicker() メソッドが用意されています。一つのファイルの読み書きであればこれらで十分です。しかしディレクトリを開いて、その下部のファイルやディレクトリに自由にアクセスしたい場合もあるでしょう。その場合、1 つのファイルにアクセスするたびにファイルピッカーが開くのは煩わしいと感じるはずです。それを解決するのが showDirectoryPicker() メソッドです。

さらに、ウェブアプリによっては、前回開いたディレクトリを最初から開いた状態にしたいと思うでしょう。実は、ユーザーからアクセスの許可を得られれば、それが実現可能です。

これらのケースは、Microsoft Visual Studio Code といった開発用テキストエディタでプロジェクトディレクトリを開くことを想像すると分かりやすいでしょう。今回は、そのようなディレクトリアクセスについて解説します。

showDirectoryPicker() メソッドでディレクトリを開く

showDirectoryPicker() メソッドは window オブジェクトのメソッドで Promise オブジェクトを返します。次の例は button 要素のボタンをクリックしたら、ディレクトリ選択ダイアログを表示します。ユーザーがディレクトリを選択したら、その直下にあるファイルとディレクトリの名前をコンソールに出力します。

<button id="btn1">ディレクトリを開く</button>
document.getElementById('btn1').addEventListener('click', async () => {
  // ディレクトリ選択ダイアログを表示して
  // FileSystemDirectoryHandle オブジェクトを取得
  const dh = await window.showDirectoryPicker();

  // 開いたディレクトリ内のファイルとディレクトリをコンソールに出力
  for await (const handle of root.values()) {
    if (handle.kind === 'file') {
      console.log(handle.name);
    } else if (handle.kind === 'directory') {
      console.log(handle.name + '/');
    }
  }
});

showDirectoryPicker() メソッドが呼び出されると、次のようなディレクトリ選択ダイアログが表示され、ユーザーが「フォルダの選択」または「キャンセル」ボタンを押すまで待ちます。

ユーザーが「キャンセル」ボタンを押すと、showDirectoryPicker() メソッドは DOMException 例外を投げます。そして、ユーザーが「フォルダーの選択」ボタンを押すと次のような許可ダイアログをが表示されます。

ユーザーがこの許可ダイアログの「キャンセル」ボタンを押すと、showDirectoryPicker() メソッドは DOMException 例外を投げます。そして、ユーザーが「ファイルを表示する」を押すと、showDirectoryPicker() メソッドはユーザーが選択したディレクトリを表す FileSystemDirectoryHandle オブジェクト(変数 dh)を返します。

FileSystemDirectoryHandle オブジェクトの使い方に関しては記事「File System Access API – 仮想的なファイルシステム Origin Private File System 編」をご覧ください。

前述のサンプルコードが期待通りに実行されると、次のような結果がコンソールに出力されます。

images/
test.html

もちろん、この結果は、どのディレクトリを選択したかによって大きく変わります。

ディレクトリ内の操作

showDirectoryPicker() メソッドでディレクトリを開くと、ブラウザーを閉じない限り、そのディレクトリ内であれば、ファイル操作を自由に行うことができます。次のサンプルは、開いたディレクトリ直下にある index.html の内容を読み取ってから、まったく別の内容に書き換えます。

<button id="btn2">ディレクトリを開く</button>
document.getElementById('btn2').addEventListener('click', async () => {
  // ディレクトリオープンダイアログを表示して
  // FileSystemDirectoryHandle オブジェクトを取得
  const dh = await window.showDirectoryPicker();

  // index.html の内容を読み取る
  const fh = await dh.getFileHandle('index.html');
  const file = await fh.getFile();
  let content = await file.text();

  // 内容を書き換える
  content = '書き換えました。';
  const stream = await fh.createWritable();
  await stream.write(content);
  await stream.close();
});

このサンプルでは、index.html に書き込みを行おうとすると、次のような許可ダイアログが表示されます。「変更を保存」を押すと、実際に index.html が書き換えられます。

FileSystemDirectoryHandle オブジェクトさえ得られれば、その配下で、サンプルのような既存ファイルの書き換えだけでなく、ファイルやディレクトリの新規生成、名前の変更、削除も可能となります。前述の許可ダイアログは、ブラウザーを閉じない限り、表示されることはありません。ただし、再読み込みすると、ユーザーの許可がリセットされ、再度、ダイアログが表示されます。

ファイルの編集、ファイルやディレクトリの新規生成、名前の変更、削除に関しては記事「File System Access API – 仮想的なファイルシステム Origin Private File System 編」をご覧ください。

選択ディレクトリの保存とユーザーの許可

これまでのサンプルをご覧いただいてお気づきかもしれませんが、ユーザーはウェブアプリにアクセスするたびにディレクトリを選択しないといけません。たとえば開発用テキストエディタのウェブアプリを想定するなら、ユーザーに選ばせることなく、前回アクセスしたディレクトリを自動的に選択したいところです。

しかし、File System Access API では選択されたディレクトリのパスを知ることはできません。仮にそのパスが分かったところで、そのパス情報からディレクトリを自動的い開く手段も用意されていません。しかし、一度ユーザーが選択したディレクトリを表す FileSystemDirectoryHandle オブジェクトが手に入ればどうでしょう。このオブジェクトさえ手に入れば、そのディレクトリ配下は自由に操作できるはずです。

実は、FileSystemDirectoryHandle オブジェクトはシリアル化可能オブジェクト(Serializable object)になっています。FileSystemDirectoryHandle オブジェクトだけでなく、ファイルを表す FileSystemFileHandle もシリアル化可能オブジェクトです。シリアル化可能オブジェクトは JSON などのテキストに変換できるというわけではありませんが、Indexed Database API の値としてストレージに保存することが可能なのです。

Indexed Database API は少し複雑なので、Web Storage のように Key-Value でデータを Indexed Database API に保存できる JS ライブラリー「IDB-Keyval」を使って、コードを見てみましょう。

<button id="btn3">ディレクトリを開く</button>
document.getElementById('btn3').addEventListener('click', async () => {
  // Indexed Database から FileSystemDirectoryHandle オブジェクトを取得
  // なければディレクトリ選択ダイアログを表示
  let dh = await idbKeyval.get('dir');
  if (!dh) {
    dh = await window.showDirectoryPicker();
  }

  // ファイルとディレクトリの一覧
  for await (const handle of dh.values()) {
    if (handle.kind === 'file') {
      console.log(handle.name);
    } else if (handle.kind === 'directory') {
      console.log(handle.name + '/');
    }
  }

  // FileSystemDirectoryHandle オブジェクトを Indexed Database に保存
  await idbKeyval.set('dir', dh);
});

このサンプルは、ボタンがクリックされたら Indexed Database から FileSystemDirectoryHandle オブジェクトを取得します。なければ、showDirectoryPicker() を呼び出し、ユーザーにディレクトリを選択させて FileSystemDirectoryHandle オブジェクトを取得します。

ディレクトリハンドルが取得できたら、そのディレクトリ直下のファイルやディレクトリの名前をコンソールに出力し、最後に、ディレクトリハンドルである FileSystemDirectoryHandle オブジェクトを Indexed Database に保存します。

このコードからお分かりの通り、このサンプルは一度ユーザーがディレクトリを選択したら、ブラウザーを閉じても、次にアクセスしたときにはディレクトリ選択ダイアログが表示されずに、いきなり前回アクセスしたディレクトリが選択された状態になるはずです。

しかし、このコードを実際に試すと、初めてアクセスしたときはファイル一覧が出力されるのですが、次にアクセスしたときには、Chrome なら次のエラーがコンソールに出力されてしまいます。

Uncaught (in promise) DOMException: The request is not allowed by the user agent or the platform in the current context.

二回目のアクセスでは、Indexed Database に保存された FileSystemDirectoryHandle オブジェクトを使い回すことになります。もしこのコードが機能してしまうと、ユーザーに何も知らせないままディレクトリにアクセスできてしまうことになります。

File System Access API では、このようなケースに対処するべく、ユーザーの許可がすでに得られているのかを調べるメソッドと、ユーザーの許可を得るメソッドを用意しています。前述のサンプルを修正すると、次のようになります。

document.getElementById('btn3').addEventListener('click', async () => {
  // Indexed Database から FileSystemDirectoryHandle オブジェクトを取得
  let dh = await idbKeyval.get('dir');

  if (dh) {
    // すでにユーザーの許可が得られているかをチェック
    let permission = await dh.queryPermission({ mode: 'readwrite' });
    if (permission !== 'granted') {
      // ユーザーの許可が得られていないなら、許可を得る(ダイアログを出す)
      permission = await dh.requestPermission({ mode: 'readwrite' });
      if (permission !== 'granted') {
        throw new Error('ユーザーの許可が得られませんでした。');
      }
    }
  } else {
    // ディレクトリ選択ダイアログを表示
    dh = await window.showDirectoryPicker();
  }

  // ファイルとディレクトリの一覧
  for await (const handle of dh.values()) {
    if (handle.kind === 'file') {
      console.log(handle.name);
    } else if (handle.kind === 'directory') {
      console.log(handle.name + '/');
    }
  }

  // FileSystemDirectoryHandle オブジェクトを Indexed Database に保存
  await idbKeyval.set('dir', dh);

});

このコードでは、Indexed Database から FileSystemDirectoryHandle オブジェクトが得られたら、まずユーザーの許可が得られているかをチェックしています。

let permission = await dh.queryPermission({ mode: 'readwrite' });

FileSystemDirectoryHandle オブジェクト(変数 dh)の queryPermission() メソッドを使って、ユーザーの許可の状況を得ます。queryPermission() に引き渡した mode プロパティには "read""readwrite" を指定することができます。デフォルト値は "read" です。ここでは "readwrite" を指定していますので、読み取りだけでなく書き込みの権限もチェックすることになります。

queryPermission() メソッドは "granted", "denied", "prompt" のいずれかを返します。ユーザーがすでに許可済みかをチェックしたいだけなら、その値が "granted" かどうかをチェックします。

もしユーザーの許可がまだ得られていない状況なら、ユーザーから許可を得なければいけません。

permission = await dh.requestPermission({ mode: 'readwrite' });

FileSystemDirectoryHandle オブジェクト(変数 dh)の requestPermission() メソッドを呼び出すと、ユーザーに許可ダイアログを表示します。

ユーザーが「変更を保存」を押せば requestPermission() メソッドは "granted" を返します。「キャンセル」を押せば "prompt" を返します。

まとめ

ユーザーが選択したディレクトリ配下を自由にアクセスできることで、ウェブアプリはデスクトップアプリにかなり近づいた感じがしないでしょうか。Microsoft 365、Google ドキュメント・スプレッドシート・スライド、Adobe のウェブ版 Photoshop・Illustrator など、デスクトップアプリと同じことがブラウザー上で実現できるようになっています。

2022 年 6 月現在、showDirectoryPicker() メソッドをサポートしているのは Chrome や Edge などの Chromium 系ブラウザーに限られますが、ローカルファイルやディレクトリへのアクセスが自由になることで、ウェブアプリ化がどんどん進んでいくのではないかと期待せずにいられません。

今回は以上で終わりです。最後まで読んでくださりありがとうございました。それでは次回の記事までごきげんよう。

Share