Does HTML5 allow drag-drop upload of folders or a folder tree? Does HTML5 allow drag-drop upload of folders or a folder tree? javascript javascript

Does HTML5 allow drag-drop upload of folders or a folder tree?


It's now possible, thanks to Chrome >= 21.

function traverseFileTree(item, path) {  path = path || "";  if (item.isFile) {    // Get file    item.file(function(file) {      console.log("File:", path + file.name);    });  } else if (item.isDirectory) {    // Get folder contents    var dirReader = item.createReader();    dirReader.readEntries(function(entries) {      for (var i=0; i<entries.length; i++) {        traverseFileTree(entries[i], path + item.name + "/");      }    });  }}dropArea.addEventListener("drop", function(event) {  event.preventDefault();  var items = event.dataTransfer.items;  for (var i=0; i<items.length; i++) {    // webkitGetAsEntry is where the magic happens    var item = items[i].webkitGetAsEntry();    if (item) {      traverseFileTree(item);    }  }}, false);

More info: https://protonet.info/blog/html5-experiment-drag-drop-of-folders/


Unfortunately none of the existing answers are completely correct because readEntries will not necessarily return ALL the (file or directory) entries for a given directory. This is part of the API specification (see Documentation section below).

To actually get all the files, we'll need to call readEntries repeatedly (for each directory we encounter) until it returns an empty array. If we don't, we will miss some files/sub-directories in a directory e.g. in Chrome, readEntries will only return at most 100 entries at a time.

Using Promises (await/ async) to more clearly demonstrate the correct usage of readEntries (since it's asynchronous), and breadth-first search (BFS) to traverse the directory structure:

// Drop handler function to get all filesasync function getAllFileEntries(dataTransferItemList) {  let fileEntries = [];  // Use BFS to traverse entire directory/file structure  let queue = [];  // Unfortunately dataTransferItemList is not iterable i.e. no forEach  for (let i = 0; i < dataTransferItemList.length; i++) {    queue.push(dataTransferItemList[i].webkitGetAsEntry());  }  while (queue.length > 0) {    let entry = queue.shift();    if (entry.isFile) {      fileEntries.push(entry);    } else if (entry.isDirectory) {      queue.push(...await readAllDirectoryEntries(entry.createReader()));    }  }  return fileEntries;}// Get all the entries (files or sub-directories) in a directory // by calling readEntries until it returns empty arrayasync function readAllDirectoryEntries(directoryReader) {  let entries = [];  let readEntries = await readEntriesPromise(directoryReader);  while (readEntries.length > 0) {    entries.push(...readEntries);    readEntries = await readEntriesPromise(directoryReader);  }  return entries;}// Wrap readEntries in a promise to make working with readEntries easier// readEntries will return only some of the entries in a directory// e.g. Chrome returns at most 100 entries at a timeasync function readEntriesPromise(directoryReader) {  try {    return await new Promise((resolve, reject) => {      directoryReader.readEntries(resolve, reject);    });  } catch (err) {    console.log(err);  }}

Complete working example on Codepen: https://codepen.io/anon/pen/gBJrOP

FWIW I only picked this up because I wasn't getting back all the files I expected in a directory containing 40,000 files (many directories containing well over 100 files/sub-directories) when using the accepted answer.

Documentation:

This behaviour is documented in FileSystemDirectoryReader. Excerpt with emphasis added:

readEntries()
Returns a an array containing some number of thedirectory's entries. Each item in the array is an object based onFileSystemEntry—typically either FileSystemFileEntry orFileSystemDirectoryEntry.

But to be fair, the MDN documentation could make this clearer in other sections. The readEntries() documentation simply notes:

readEntries() method retrieves the directory entries within the directory being read and delivers them in an array to the provided callback function

And the only mention/hint that multiple calls are needed is in the description of successCallback parameter:

If there are no files left, or you've already called readEntries() onthis FileSystemDirectoryReader, the array is empty.

Arguably the API could be more intuitive as well, but as the documentation notes: it is a non-standard/experimental feature, not on a standards track, and can't be expected to work for all browsers.

Related:

  • johnozbay comments that on Chrome, readEntries will return at most 100 entries for a directory (verified as Chrome 64).
  • Xan explains the correct usage of readEntries quite well in this answer (albeit without code).
  • Pablo Barría Urenda's answer correctly calls readEntries in a asynchronous manner without BFS. He also notes that Firefox returns all the entries in a directory (unlike Chrome) but we can't rely on this given the specification.


This function will give you a promise for array of all dropped files, like <input type="file"/>.files:

function getFilesWebkitDataTransferItems(dataTransferItems) {  function traverseFileTreePromise(item, path='') {    return new Promise( resolve => {      if (item.isFile) {        item.file(file => {          file.filepath = path + file.name //save full path          files.push(file)          resolve(file)        })      } else if (item.isDirectory) {        let dirReader = item.createReader()        dirReader.readEntries(entries => {          let entriesPromises = []          for (let entr of entries)            entriesPromises.push(traverseFileTreePromise(entr, path + item.name + "/"))          resolve(Promise.all(entriesPromises))        })      }    })  }  let files = []  return new Promise((resolve, reject) => {    let entriesPromises = []    for (let it of dataTransferItems)      entriesPromises.push(traverseFileTreePromise(it.webkitGetAsEntry()))    Promise.all(entriesPromises)      .then(entries => {        //console.log(entries)        resolve(files)      })  })}

Usage:

dropArea.addEventListener("drop", function(event) {  event.preventDefault();  var items = event.dataTransfer.items;  getFilesFromWebkitDataTransferItems(items)    .then(files => {      ...    })}, false);

NPM package:https://www.npmjs.com/package/datatransfer-files-promise

Usage example:https://github.com/grabantot/datatransfer-files-promise/blob/master/index.html