タイトルの通りです。
ブラウザにドロップしたディレクトリ内のファイルを一括取得したいと思ったので、その方法を自分なりに調べてみました。
個人的になかなか難しかったので、忘れないようにブログという形で残すことにしました。
まずは、この記事の中で目指す目標を紹介します。
- ディレクトリ丸ごと画面にドロップ!中のファイルを全て取得したい
- サブディレクトリの中のファイルまで取得できれば完璧
- できればパスも知りたい
なかなかの欲張りセットです。今回の記事では javaScript を用いてこれらの機能を実装していきます。ライブラリとして react.js を利用しますが、上記機能の実装に影響することはほとんどありません。ドラッグ&ドロップの実装が個人的に簡単になるくらいのイメージです。
既に react 以外でドラッグ&ドロップを実装できる方は、以下の手順1をスキップしても良いかもしれません。
- ディレクトリ(or ファイル)をドロップできる環境を作る
- ファイル単体をドロップ、File オブジェクトを取得してみる
- ディレクトリをドロップ、全てのファイルを取得してみる
- OS:windows 10
- editor:vscode
- react関係
- Node:v14.16.1
- npm:6.13.4
- 「create-react-app」でファイル自動生成
こちら、ドラッグ&ドロップに問題なしの方は読み飛ばして貰って構いません。ライブラリとして React.js を利用しています。
新しくコンポーネントを作ります。コンポーネント名は「DropDirectory」にしておきます。
import React from "react"; |
function DropDirectory() { |
return ( |
<div style={{ width: 600, height: 300, border: "solid", borderWidth: 2 }}> |
ドロップエリアですぅ |
</div> |
); |
} |
export { DropDirectory } |
新しく「DropArea コンポーネント」を作成します。機能はシンプルです。ブラウザにディレクトリ(or ファイル)をドロップした際に動く既定の処理を停止させ、任意の関数を実行できるようにします。
引数「children」にはDropArea コンポーネントが囲った子コンポーネントが入ります。
import React from "react"; |
function DropArea({ children, onDrop }) { |
const handleDragOver = (e) => { |
e.stopPropagation(); |
e.preventDefault(); |
}; |
const handleDrop = (e) => { |
e.stopPropagation(); |
e.preventDefault(); |
onDrop(e); |
}; |
return ( |
<div onDragOver={handleDragOver} onDrop={handleDrop}> |
{children} |
</div> |
); |
export { DropArea } |
import React from "react"; |
import { DropArea } from "./DropArea"; // コンポーネントのインポート |
function DropDirectory() { |
const handleDrop = (event) => {}; |
return ( |
<DropArea onDrop={handleDrop}> |
<div style={{ width: 600, height: 300, border: "solid", borderWidth: 2 }}> |
ドロップエリアですぅ |
</div> |
</DropArea> |
); |
} |
export { DropDirectory } |
以上です。これでディレクトリやファイルをドラッグ&ドロップする準備が整いました。次からは実際にファイルを取得してみようと思います。
ファイルをドロップした際に呼ばれるコールバック関数を記述しましょう。
手順1で作成したコードでいうと、DropDirectory コンポーネントの「handleDrop」がこれに当たります。
以下の流れでファイルを取得できます。
const handleDrop = async (e) => { |
const item = e.dataTransfer.items[0]; // ※1 |
const entry = item.webkitGetAsEntry(); // ※2 |
if (entry.isFile) { |
const file = await new Promise((resolve) => { |
entry.file((file) => { // ※3 |
resolve(file); |
}); |
}); |
console.log("これはファイルです", file); |
} else if (entry.isDirectory) { |
console.log("これはディレクトリです"); |
} |
}; |
※1.e.dataTransfer.items[0]
:複数ディレクトリ(or ファイル)をドロップした際、その先頭のディレクトリ(or ファイル)のみ取得する。この時点ではまだ File オブジェクトではなく、「DataTransferItem オブジェクト」として扱われる。
※2.webkitGetAsEntry()
:DataTransferItem オブジェクトを「FileSystemFileEntry オブジェクト」もしくは「FileSystemDirectoryEntry オブジェクト」に変換する。変換された 後は、上記コードのように「isFile」「isDerectory」によってどちらのEntry かを判別することができる。
※3:FileSystemFileEntry オブジェクトから File オブジェクトを取得する。非同期での動作なので、処理が完了した後でログを表示させている。
ディレクトリの中には、ファイルとサブディレクトリがあります。
サブディレクトリの中にも、ファイルとサブディレクトリがあります。
よって、ディレクトリがドロップされた際には、ファイルとディレクトリを識別するスキャン関数を再帰的に呼び出す必要があります。コールバック関数を次のように変更します。
const handleDrop = async (e) => { |
const item = e.dataTransfer.items[0]; |
const entry = item.webkitGetAsEntry(); |
const fileList = []; // 取得したファイルを格納するリスト |
// ファイルスキャン関数 |
const traverseFileTree = async (entry, path) => { |
const _path = path || ""; |
if (entry.isFile) { |
const file = await new Promise((resolve) => { |
entry.file((file) => { |
resolve(file); |
}); |
}); |
fileList.push({ file: file, path: _path + file.name }); // ファイルを取得したらリストにプッシュする |
} else if (entry.isDirectory) { |
const directoryReader = entry.createReader(); |
const entries = await new Promise((resolve) => { |
directoryReader.readEntries((entries) => { |
resolve(entries); |
}); |
}); |
for (let i = 0; i < entries.length; i++) { |
await traverseFileTree(entries[i], _path + entry.name + "/"); // 再帰的な関数呼び出し |
} |
} |
}; |
// ここでドロップされた最初のディレクトリ(or ファイル)を渡す |
await traverseFileTree(entry); |
// 最後にファイル一覧をログに出力 |
console.log(fileList); |
}; |
一応これで、今回の目標は達成できたと思います。しかし、せっかく読み込んだファイルの使い道がログへの表示だけというのも、ちょっと味気ないですよね。オーディオファイルを再生してみたり、画像ファイルを表示してみたり、どうぞ好きなように使ってみてください。
まだまだ説明不足な点もあるかもしれませんが、この記事が誰かの役に立てれば幸いです。