JavaScript で <input> から複数ディレクトリを読み込む

## 背景

「単一/複数ファイル読み込み」「単一ディレクトリ読み込み」の方法はすんなり見つかるが、「複数ディレクトリ読み込み」が全然見つからない･･･。

しかし、Google Drive などでは、複数のディレクトリを一気にアップロードすることができるため、方法は無いはずは無い･･･。

色々と探した結果、`webkitEntries` というものをやりくりすることでこれが実現できると分かり、その手順を記すことにしました。

## 完成したコード

これは、`webkitEntries` からファイルを `File` オブジェクトで得るコードです。`File` オブジェクトに変換できれば、あとは自由に使えます。

:::note warn
**制約事項**

* `webkitDirectory` プロパティが指定されているとき、この方法は使えない
  * 後述の `webkitEntries` が空 Array になる
* webkit という名がいつか改名される可能性はある
  * もともと Chrome 独自 API だったため
* Opera ではできないかも
  * [後述の `.createReader()` メソッドが非対応らしい？](https://developer.mozilla.org/ja/docs/Web/API/FileSystemDirectoryEntry#%E3%83%96%E3%83%A9%E3%82%A6%E3%82%B6%E3%81%AE%E4%BA%92%E6%8F%9B%E6%80%A7) (記事執筆時点)
* HTTP/HTTPS 接続上でないと使えない (ローカル HTML ファイル読み込みでは不可)
  * ローカルファイルだと FileSystemEntry API が使えない
:::

※ 簡単のため、エラーチェック等は省いています。

```html
<input type="file" id="file-in" multiple>
```

```js
document.querySelector("#file-in").addEventListener("change" async e => {
  const files = [];
  for(const entry of e.target.webkitEntries) {
    files.push(...await files_from_entry(entry));
  }
  // files: 最終的に得られたファイル
  console.log(files);
});

async function files_from_entry(entry) {
  if(entry.isDirectory) {
    // DirectoryEntry -> Directory/FileEntries (child entries)
    const child_entries = await new Promise(resolve => {
      entry.createReader().readEntries(entries => { resolve(entries); });
    });
    // Child entries -> File objects
    let files = [];
    for(const child_entry of child_entries) {
      files.push(...await files_from_entry(child_entry));
    }
    return files;

  } else {
    // FileEntry -> File object
    let file = await new Promise(resolve => {
      entry.file(f => { resolve(f); });
    });
    return [file];
    // ディレクトリ名を含めたファイル名を得るには entry.fullPath
    // 例: return [{file: file, path: entry.fullPath}]
  }
}
```

## 解説

### FileSystemEntry 取得

```js
// change イベントの発生、すなわち入力またはキャンセルされたときに実行
document.querySelector("#file-in").addEventListener("change", e => {
  let entries = e.target.webkitEntries;

  // ちなみに次と等価
  let entries = document.querySelector("#file-in").webkitEntries;
});
```

input 要素の [`webkitEntries`](https://developer.mozilla.org/ja/docs/Web/API/HTMLInputElement/webkitEntries) プロパティは Array で、要素に [`FileSystemFileEntry`](https://developer.mozilla.org/ja/docs/Web/API/FileSystemFileEntry) か [`FileSystemDirectoryEntry`](https://developer.mozilla.org/ja/docs/Web/API/FileSystemDirectoryEntry) を持ちます。前者は入力されたファイルに、後者はディレクトリに該当します。

### ファイル/ディレクトリエントリの分岐

```js
if(entry.isDirectory) {
  //
}
```

`.isDirectory` メンバが true ならディレクトリエントリの処理を、false ならファイルエントリの処理を進めます。

### `FileSystemDirectoryEntry` → 子要素のエントリ

```js
const child_entries = await new Promise(resolve => {
  entry.createReader().readEntries(entries => { resolve(entries); });

  // 次と等価
  // '=>' は アロー関数式
  entry.createReader().readEntries(function(entries) { resolve(entries); });
});

let files = [];
for(const child_entry of child_entries) {
  files.push(...await files_from_entry(child_entry));
}
```

`FileSystemDirectoryEntry` オブジェクトからリーダを作成し、ディレクトリ内にある全子要素のエントリを取得します。

[`.readEntries()`](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryReader/readEntries) メソッドの引数には関数を入れます。その関数は、得られたエントリを要素に持つ `Array` オブジェクトを引数に渡して呼ばれます。

ここで、非同期処理に関する命令 `Promise` `await` を活用することで、処理の完了を待ってから次に進むようにしています。(非同期処理の解説はここではカット)

得られたエントリそれぞれに対し、それがディレクトリエントリなら同じように子要素読み込み、ファイルエントリなら次の `File` オブジェクトへの変換処理に進みます。これを関数の再帰で実装することで、コードはコンパクトなまま多階層のディレクトリに対応しています。

### `FileSystemFileEntry` → `File` オブジェクト

```js
let file = await new Promise(resolve => {
  entry.file(f => { resolve(f); });

  // 次と等価
  entry.file(function(f) { resolve(f); });
});
```

上で得られた `FileSystemFileEntry` から `File` オブジェクトを得ます。ここでも `Promise` `await` で処理を待つ手法を適用しています。

これでゴールとなる `File` オブジェクトが得られました。

## おわりに

こういうのさえあったら話はもっと楽なのに･･･。

```html
<!-- ※ 存在しないプロパティ -->
<input type="file" id="file-in" multiple-directory>
```

ちなみに multiple と webkitdirectory 両方は反映されない。

```html
<input type="file" id="file-in" multiple webkitdirectory>
```