0
0

More than 1 year has passed since last update.

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

Posted at

背景

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

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

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

完成したコード

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

制約事項

  • webkitDirectory プロパティが指定されているとき、この方法は使えない
    • 後述の webkitEntries が空 Array になる
  • webkit という名がいつか改名される可能性はある
    • もともと Chrome 独自 API だったため
  • Opera ではできないかも
  • 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 で、要素に FileSystemFileEntryFileSystemDirectoryEntry を持ちます。前者は入力されたファイルに、後者はディレクトリに該当します。

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

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 オブジェクトへの変換処理に進みます。これを関数の再帰で実装することで、コードはコンパクトなまま多階層のディレクトリに対応しています。

FileSystemFileEntryFile オブジェクト

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>
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0