背景
「単一/複数ファイル読み込み」「単一ディレクトリ読み込み」の方法はすんなり見つかるが、「複数ディレクトリ読み込み」が全然見つからない・・・。
しかし、Google Drive などでは、複数のディレクトリを一気にアップロードすることができるため、方法は無いはずは無い・・・。
色々と探した結果、webkitEntries
というものをやりくりすることでこれが実現できると分かり、その手順を記すことにしました。
完成したコード
これは、webkitEntries
からファイルを File
オブジェクトで得るコードです。File
オブジェクトに変換できれば、あとは自由に使えます。
制約事項
-
webkitDirectory
プロパティが指定されているとき、この方法は使えない- 後述の
webkitEntries
が空 Array になる
- 後述の
- webkit という名がいつか改名される可能性はある
- もともと Chrome 独自 API だったため
- Opera ではできないかも
-
後述の
.createReader()
メソッドが非対応らしい? (記事執筆時点)
-
後述の
- HTTP/HTTPS 接続上でないと使えない (ローカル HTML ファイル読み込みでは不可)
- ローカルファイルだと FileSystemEntry API が使えない
※ 簡単のため、エラーチェック等は省いています。
<input type="file" id="file-in" multiple>
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 取得
// change イベントの発生、すなわち入力またはキャンセルされたときに実行
document.querySelector("#file-in").addEventListener("change", e => {
let entries = e.target.webkitEntries;
// ちなみに次と等価
let entries = document.querySelector("#file-in").webkitEntries;
});
input 要素の webkitEntries
プロパティは Array で、要素に FileSystemFileEntry
か FileSystemDirectoryEntry
を持ちます。前者は入力されたファイルに、後者はディレクトリに該当します。
ファイル/ディレクトリエントリの分岐
if(entry.isDirectory) {
//
}
.isDirectory
メンバが true ならディレクトリエントリの処理を、false ならファイルエントリの処理を進めます。
FileSystemDirectoryEntry
→ 子要素のエントリ
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()
メソッドの引数には関数を入れます。その関数は、得られたエントリを要素に持つ Array
オブジェクトを引数に渡して呼ばれます。
ここで、非同期処理に関する命令 Promise
await
を活用することで、処理の完了を待ってから次に進むようにしています。(非同期処理の解説はここではカット)
得られたエントリそれぞれに対し、それがディレクトリエントリなら同じように子要素読み込み、ファイルエントリなら次の File
オブジェクトへの変換処理に進みます。これを関数の再帰で実装することで、コードはコンパクトなまま多階層のディレクトリに対応しています。
FileSystemFileEntry
→ File
オブジェクト
let file = await new Promise(resolve => {
entry.file(f => { resolve(f); });
// 次と等価
entry.file(function(f) { resolve(f); });
});
上で得られた FileSystemFileEntry
から File
オブジェクトを得ます。ここでも Promise
await
で処理を待つ手法を適用しています。
これでゴールとなる File
オブジェクトが得られました。
おわりに
こういうのさえあったら話はもっと楽なのに・・・。
<!-- ※ 存在しないプロパティ -->
<input type="file" id="file-in" multiple-directory>
ちなみに multiple と webkitdirectory 両方は反映されない。
<input type="file" id="file-in" multiple webkitdirectory>