Both mozilla and webkit browsers now allow directory upload. When directory or directories are selected at <input type="file">
element or dropped at an element, how to list all directories and files in the order which they appear in actual directory at both firefox and chrome/chromium, and perform tasks on files when all uploaded directories have been iterated?
Short summary: You can set webkitdirectory
attributes on <input type="file">
element; attach change
, drop
events to it; use .createReader()
, .readEntries()
to get all selected/dropped files and folders, and iterate over them using e.g. Array.prototype.reduce()
, Promise
, and recursion.
Note that really 2 different APIs are at play here:
webkitdirectory
feature for <input type="file">
with its change
event.
DataTransferItem.webkitGetAsEntry()
with its drop
event, which is part of the Drag-and-Drop API.
Both of them work in Firefox even though they have "webkit" in the name.
Both of them handle folder/directory hierarchies.
As stated, if you need to support empty folders, you MUST force your users to use drag-and-drop instead the OS folder chooser shown when the <input type="file">
is clicked.
An <input type="file">
that also accepts drag-and-drop into a larger area.
<!DOCTYPE html>
<html>
<head>
<style type="text/css">
input[type="file"] {
width: 98%;
height: 180px;
}
label[for="file"] {
width: 98%;
height: 180px;
}
.area {
display: block;
border: 5px dotted #ccc;
text-align: center;
}
.area:after {
display: block;
border: none;
white-space: pre;
content: "Drop your files or folders here!\aOr click to select files folders";
pointer-events: none; /* see note [drag-target] */
position: relative;
left: 0%;
top: -75px;
text-align: center;
}
.drag {
border: 5px dotted green;
background-color: yellow;
}
#result ul {
list-style: none;
margin-top: 20px;
}
#result ul li {
border-bottom: 1px solid #ccc;
margin-bottom: 10px;
}
#result li span {
font-weight: bold;
color: navy;
}
</style>
</head>
<body>
<!-- Docs of `webkitdirectory:
https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/webkitdirectory
-->
<!-- Note [drag-target]:
When you drag something onto a <label> of an <input type="file">,
it counts as dragging it on the <input>, so the resulting
`event` will still have the <input> as `.target` and thus
that one will have `.webkitdirectory`.
But not if the <label> has further other nodes in it (e.g. <span>
or plain text nodes), then the drag event `.target` will be that node.
This is why we need `pointer-events: none` on the
"Drop your files or folder here ..." text added in CSS above:
So that that text cannot become a drag target, and our <label> stays
the drag target.
-->
<label id="dropArea" class="area">
<input id="file" type="file" directory webkitdirectory />
</label>
<output id="result">
<ul></ul>
</output>
<script>
var dropArea = document.getElementById("dropArea");
var output = document.getElementById("result");
var ul = output.querySelector("ul");
function dragHandler(event) {
event.stopPropagation();
event.preventDefault();
dropArea.className = "area drag";
}
function filesDroped(event) {
var processedFiles = [];
console.log(event);
event.stopPropagation();
event.preventDefault();
dropArea.className = "area";
function handleEntry(entry) {
// See https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/webkitGetAsEntry
let file =
"getAsEntry" in entry ? entry.getAsEntry() :
"webkitGetAsEntry" in entry ? entry.webkitGetAsEntry()
: entry;
return Promise.resolve(file);
}
function handleFile(entry) {
return new Promise(function(resolve) {
if (entry.isFile) {
entry.file(function(file) {
listFile(file, entry.fullPath).then(resolve)
})
} else if (entry.isDirectory) {
var reader = entry.createReader();
reader.readEntries(webkitReadDirectories.bind(null, entry, handleFile, resolve))
} else {
var entries = [entry];
return entries.reduce(function(promise, file) {
return promise.then(function() {
return listDirectory(file)
})
}, Promise.resolve())
.then(function() {
return Promise.all(entries.map(function(file) {
return listFile(file)
})).then(resolve)
})
}
})
function webkitReadDirectories(entry, callback, resolve, entries) {
console.log(entries);
return listDirectory(entry).then(function(currentDirectory) {
console.log(`iterating ${currentDirectory.name} directory`, entry);
return entries.reduce(function(promise, directory) {
return promise.then(function() {
return callback(directory)
});
}, Promise.resolve())
}).then(resolve);
}
}
function listDirectory(entry) {
console.log(entry);
var path = (entry.fullPath || entry.webkitRelativePath.slice(0, entry.webkitRelativePath.lastIndexOf("/")));
var cname = path.split("/").filter(Boolean).join("-");
console.log("cname", cname)
if (!document.getElementsByClassName(cname).length) {
var directoryInfo = `<li><ul class=${cname}>
<li>
<span>
Directory Name: ${entry.name}<br>
Path: ${path}
<hr>
</span>
</li></ul></li>`;
var curr = document.getElementsByTagName("ul");
var _ul = curr[curr.length - 1];
var _li = _ul.querySelectorAll("li");
if (!document.querySelector("[class*=" + cname + "]")) {
if (_li.length) {
_li[_li.length - 1].innerHTML += `${directoryInfo}`;
} else {
_ul.innerHTML += `${directoryInfo}`
}
} else {
ul.innerHTML += `${directoryInfo}`
}
}
return Promise.resolve(entry);
}
function listFile(file, path) {
path = path || file.webkitRelativePath || "/" + file.name;
var filesInfo = `<li>
Name: ${file.name}</br>
Size: ${file.size} bytes</br>
Type: ${file.type}</br>
Modified Date: ${file.lastModifiedDate}<br>
Full Path: ${path}
</li>`;
var currentPath = path.split("/").filter(Boolean);
currentPath.pop();
var appended = false;
var curr = document.getElementsByClassName(`${currentPath.join("-")}`);
if (curr.length) {
for (li of curr[curr.length - 1].querySelectorAll("li")) {
if (li.innerHTML.indexOf(path.slice(0, path.lastIndexOf("/"))) > -1) {
li.querySelector("span").insertAdjacentHTML("afterend", `${filesInfo}`);
appended = true;
break;
}
}
if (!appended) {
curr[curr.length - 1].innerHTML += `${filesInfo}`;
}
}
console.log(`reading ${file.name}, size: ${file.size}, path:${path}`);
processedFiles.push(file);
return Promise.resolve(processedFiles)
};
function processFiles(files) {
Promise.all([].map.call(files, function(file, index) {
return handleEntry(file, index).then(handleFile)
}))
.then(function() {
console.log("complete", processedFiles)
})
.catch(function(err) {
alert(err.message);
})
}
var files;
if (event.type === "drop" && event.target.webkitdirectory) {
files = event.dataTransfer.items || event.dataTransfer.files;
} else if (event.type === "change") {
files = event.target.files;
}
if (files) {
processFiles(files)
}
}
dropArea.addEventListener("dragover", dragHandler);
dropArea.addEventListener("change", filesDroped);
dropArea.addEventListener("drop", filesDroped);
</script>
</body>
</html>
Live demo: https://plnkr.co/edit/hUa7zekNeqAuwhXi
Old text (now edited out): Firefox drop
event does not list selection as a Directory
, but a File
object having size
0
, thus dropping directory at firefox does not provide representation of dropped folder, even where event.dataTransfer.getFilesAndDirectories()
is utilized.
This was fixed with Firefox 50, which added webkitGetAsEntry
support (changelog, issue).
Firefox once had on <input type="file">
(HTMLInputElement
) the function .getFilesAndDirectories()
(added in this commit, issue). It was available only when the about:config
preference dom.input.dirpicker
was set (which was only on in Firefox Nightly, and removed again in Firefox 101, see other point below). It was removed again (made testing-only) in this commit.
Check out this post for the history of webkitdirectory
and HTMLInputElement.getFilesAndDirectories()
.
Old text (now edited out): Firefox provides two input elements when allowdirs
attribute is set; the first element allows single file uploads, the second element allows directory upload. chrome/chromium provide single <input type="file">
element where only single or multiple directories can be selected, not single file.
The allowdirs
feature was removed in Firefox 101 (code, issue). Before that, it was available via an off-by-default about:config
setting dom.input.dirpicker
. It was made off-by-default in Firefox 50: (code, issue). Before, it was on-by-default only in Firefox Nightly.
This means that now, Firefox ignores the allowdirs
attribute, and when clicking the Choose file
button, it displays a directory-only picker (same behaviour as Chrome).
The webkitdirectory
feature for <input type="file">
currently works everywhere except:
DataTransferItem.webkitGetAsEntry()
currently works everywhere except:
DataTransferItem.webkitGetAsEntry()
docs say:
This function is implemented as
webkitGetAsEntry()
in non-WebKit browsers including Firefox at this time; it may be renamed togetAsEntry()
in the future, so you should code defensively, looking for both.