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

JavascriptHtmlFile UploadDrag and-Drop

Javascript Problem Overview


I haven't seen any examples that do this. Is this not allowed in the API spec?

I am searching for an easy drag-drop solution for uploading an entire folder tree of photos.

Javascript Solutions


Solution 1 - Javascript

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/

Solution 2 - Javascript

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 files
async 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 array
async 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 time
async 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 the > directory's entries. Each item in the array is an object based on > FileSystemEntry—typically either FileSystemFileEntry or > FileSystemDirectoryEntry.

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() on > this FileSystemDirectoryReader, the array is empty.

Arguably the API could be more intuitive as well.

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.

Solution 3 - Javascript

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

Solution 4 - Javascript

In this message to the HTML 5 mailing list Ian Hickson says:

> HTML5 now has multiple> to upload many files at > once. Browsers could allow users to > pick multiple files at once, including > across multiple directories; that's a > bit out of scope of the spec.

(Also see the original feature proposal.) So it's safe to assume he considers uploading folders using drag-and-drop also out of scope. Apparently it's up to the browser to serve individual files.

Uploading folders would also have some other difficulties, as described by Lars Gunther:

> This […] proposal must have two > checks (if it is doable at all): > > 1. Max size, to stop someone from uploading a full directory of several > hundred uncompressed raw images... > > 2. Filtering even if the accept attribute is omitted. Mac OS metadata > and Windows thumbnails, etc should be > omitted. All hidden files and > directories should default to be > excluded.

Solution 5 - Javascript

Now you can upload directories with both drag and drop and input.

<input type='file' webkitdirectory >

and for drag and drop(For webkit browsers).

Handling drag and drop folders.

<div id="dropzone"></div>
<script>
var dropzone = document.getElementById('dropzone');
dropzone.ondrop = function(e) {
  var length = e.dataTransfer.items.length;
  for (var i = 0; i < length; i++) {
    var entry = e.dataTransfer.items[i].webkitGetAsEntry();
    if (entry.isFile) {
      ... // do whatever you want
    } else if (entry.isDirectory) {
      ... // do whatever you want
    }
  }
};
</script>

Resources:

http://updates.html5rocks.com/2012/07/Drag-and-drop-a-folder-onto-Chrome-now-available

Solution 6 - Javascript

Firefox now supports folder upload, as of November 15, 2016, in v50.0: https://developer.mozilla.org/en-US/Firefox/Releases/50#Files_and_directories

You can drag and drop folders into Firefox or you can browse and select a local folder to upload. It also supports folders nested in subfolders.

That means you can now use either Chrome, Firefox, Edge or Opera to upload folders. You can't use Safari or Internet Explorer at present.

Solution 7 - Javascript

Here's a complete example of how to use the file and directory entries API:

var dropzone = document.getElementById("dropzone");
var listing = document.getElementById("listing");

function scanAndLogFiles(item, container) {
  var elem = document.createElement("li");
  elem.innerHTML = item.name;
  container.appendChild(elem);

  if (item.isDirectory) {
    var directoryReader = item.createReader();
    var directoryContainer = document.createElement("ul");
    container.appendChild(directoryContainer);

    directoryReader.readEntries(function(entries) {
      entries.forEach(function(entry) {
        scanAndLogFiles(entry, directoryContainer);
      });
    });
  }
}

dropzone.addEventListener(
  "dragover",
  function(event) {
    event.preventDefault();
  },
  false
);

dropzone.addEventListener(
  "drop",
  function(event) {
    var items = event.dataTransfer.items;

    event.preventDefault();
    listing.innerHTML = "";

    for (var i = 0; i < items.length; i++) {
      var item = items[i].webkitGetAsEntry();

      if (item) {
        scanAndLogFiles(item, listing);
      }
    }
  },
  false
);

body {
  font: 14px "Arial", sans-serif;
}

#dropzone {
  text-align: center;
  width: 300px;
  height: 100px;
  margin: 10px;
  padding: 10px;
  border: 4px dashed red;
  border-radius: 10px;
}

#boxtitle {
  display: table-cell;
  vertical-align: middle;
  text-align: center;
  color: black;
  font: bold 2em "Arial", sans-serif;
  width: 300px;
  height: 100px;
}

<p>Drag files and/or directories to the box below!</p>

<div id="dropzone">
  <div id="boxtitle">
    Drop Files Here
  </div>
</div>

<h2>Directory tree:</h2>

<ul id="listing"></ul>

webkitGetAsEntry is supported by Chrome 13+, Firefox 50+ and Edge.

Source: https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/webkitGetAsEntry

Solution 8 - Javascript

UPDATE: Since 2012 a lot has changed, see answers above instead. I leave this answer here for the sake of archeology.

The HTML5 spec does NOT say that when selecting a folder for upload, the browser should upload all contained files recursively.

Actually, in Chrome/Chromium, you can upload a folder, but when you do it, it just uploads a meaningless 4KB file, which represents the directory. Some servers-side applications like Alfresco can detect this, and warn the user that folders can not be uploaded:

The following cannot be uploaded because they are either folders or are zero bytes in size: undefined

Solution 9 - Javascript

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

Only Chrome supports this feature. It has failed to have any traction and is likely to be removed.

Ref : https://developer.mozilla.org/en/docs/Web/API/DirectoryReader#readEntries

Solution 10 - Javascript

Recently stumbled upon the need to implement this in two of my projects so I created a bunch of utility functions to help with this.

One creates a data-structure representing all the folders, files and relationship between them, like so 

{
  folders: [
    {
      name: string,
      folders: Array,
      files: Array
    },
    /* ... */
  ],
  files: Array
}

While the other just returns an Array of all the files (in all folders and sub-folders).

Here's the link to the package: https://www.npmjs.com/package/file-system-utils

Solution 11 - Javascript

I had been happy copy/pasting @grabantot 's solution until I met the 100 file limit issue.

@xlm 's solution overcomes the 100-file-limit, and it returns an array of FileEntry objects.

However in my project I need to extract the file paths from fileEntry objects.

This works if you have access to the ChromeFileSystem api:


const getAllPaths = async (dataTransferItems) =>{

    async function getAllFileEntries(dataTransferItemList) {
        let fileEntries = [];
        // Use BFS to traverse entire directory/file structure
        let queue = [];
       
        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 array
      async 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 time
      async function readEntriesPromise(directoryReader) {
        try {
          return await new Promise((resolve, reject) => {
            directoryReader.readEntries(resolve, reject);
          });
        } catch (err) {
          console.log(err);
        }
      }


     const getDisplayPath = (entry)=>{
        return new Promise((resolve, reject) =>{
            chrome.fileSystem.getDisplayPath(entry, (path)=>{
                if(chrome.runtime.lastError) {
                    reject(chrome.runtime.lastError)
                }else {
                    resolve(path);
                }
            })
        })
    }


    
    const fileEnties = await getAllFileEntries(dataTransferItems);

    const files = await Promise.all(fileEnties.map(async(x)=>{
        return (await getDisplayPath(x))
    }))

    return files;

}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionmichaelView Question on Stackoverflow
Solution 1 - JavascriptChristopher BlumView Answer on Stackoverflow
Solution 2 - JavascriptxlmView Answer on Stackoverflow
Solution 3 - JavascriptgrabantotView Answer on Stackoverflow
Solution 4 - JavascriptMarcel KorpelView Answer on Stackoverflow
Solution 5 - JavascriptkongarajuView Answer on Stackoverflow
Solution 6 - JavascriptDan RobertsView Answer on Stackoverflow
Solution 7 - JavascriptPaolo MorettiView Answer on Stackoverflow
Solution 8 - JavascriptNicolas RaoulView Answer on Stackoverflow
Solution 9 - JavascriptbasaratView Answer on Stackoverflow
Solution 10 - JavascriptPavaView Answer on Stackoverflow
Solution 11 - JavascriptCao ShouguangView Answer on Stackoverflow