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

11

105

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.

Unbolted answered 28/8, 2010 at 8:33 Comment(3)
Same for input type=file: #9518835Gelinas
npm package npmjs.com/package/datatransfer-files-promiseErleneerlewine
so far only in chromiums and webkits; the DataTransferItem interface returns a FileSystemFileHandle if the dragged item is a file, or a FileSystemDirectoryHandle if the dragged item is a directoryOdom
B
93

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/

As a note (from the comments) this code is not complete if more than 100 entries are returned, some iteration is required, see https://mcmap.net/q/204094/-does-html5-allow-drag-drop-upload-of-folders-or-a-folder-tree

Briarroot answered 10/7, 2012 at 9:35 Comment(12)
Even 2 years later, IE and Firefox don't seem to be willing to implement this.Benignity
question on how to use on Chrome: #5826786Gelinas
@ChristopherBlum : How do to the same for download without using zipped folders ?Serb
Now, for Firefox as well: https://mcmap.net/q/205691/-html5-drag-and-drop-folder-detection-in-firefox-is-it-even-possible It shows folder uploading via drag'n'drop and via dialog in chrome and firefox!Vitrics
Updated link to mentioned article: protonet.com/blog/html5-experiment-drag-drop-of-foldersRainey
Edge supports this too.Anandrous
Important warning: The code in this answer is limited to 100 files in a given directory. See here : bugs.chromium.org/p/chromium/issues/detail?id=514087Britzka
@Britzka it's unfortunate that more people picked up your important warning, and it's not necessarily a Chromium issue since the spec says readEntries won't return all the entires in a directory. Based on the bug link your provided, I've written up a complete answer: https://mcmap.net/q/204094/-does-html5-allow-drag-drop-upload-of-folders-or-a-folder-treeConger
The comment above about "limited to 100 files in a given directory" is invalid per the comments on the bug that is linked to.Ester
@MichaelRush The comment IS valid. From the bug issue page: the dev comments that "[t]he key is continuing to call readEntries as long as it keeps returning items". Sure the root cause was readEntries clobbering each other (same as in https://mcmap.net/q/205692/-file-and-directory-entries-api-broken-in-chrome) but it still stands that readEntries will not necessarily return all entries in a directory. The specification even provides this (see https://mcmap.net/q/204094/-does-html5-allow-drag-drop-upload-of-folders-or-a-folder-tree)Conger
@Conger Ok, thanks for the clarification! I see the issue now, a potentially misleading implementation. Also thanks for your work demonstrating a working solution.Ester
How do I create a FileList from this?Dermatology
C
93

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++) {
    // Note webkitGetAsEntry a non-standard feature and may change
    // Usage is necessary for handling directories
    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/pen/QWmvxwV

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.

It's also worth noting that DataTransferItem.webkitGetAsEntry() is a non-standard feature and may change e.g. renamed getAsEntry(). Its usage is necessary to handle uploading files nested within directories.

Related:

  • johnozbay comments that on Chrome, readEntries will return at most 100 entries for a directory (verified as of 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.
Conger answered 30/10, 2018 at 6:31 Comment(13)
Thanks a lot for the shout-out, and getting this content out there. SOF needs more fantastic members like yourself! ✌🏻Britzka
I appreciate that @Britzka I'm just concerned that it seems that many users are overlooking this small but significant fact re: specification/API and this edge case (100+ files in a directory) isn't that unlikely. I only realised it when I wasn't getting back all the files I expected. Your comment should have been answer.Conger
How to get the file size?Repudiate
To get all relevant metadata (size, lastModified, mime type), you need to convert all FileSystemFileEntry to File, via the file(successCb, failureCb) method. If you also need the full path, you should take that from fileEntry.fullPath (file.webkitRelativePath will be just the name).Sensualism
This seems like the best answer, but does not work for me in Chromium 86. Seems to work fine in Firefox. In Chromium it will upload selections containing files, but nothing is uploaded for a directory because readEntriesPromise() returns an empty array.Tele
@Tele I just tested the Codepen with Chrome 86 and it's still working. We expect readEntriesPromise to return an empty array, eventually. We have to call it in loop (from readAllDirectoryEntries) until it returns an empty. Then we know we have collected all the entries (files and subdirectories) in entries. readAllDirectoryEntries itself must be called using BFS or similar (e.g. getAllFileEntries) since directories can contain subdirectories and those can further subdirectories and so on. If you're still stuck please post a new Q and I'll try to take a lookConger
Interesting, the same code runs in the codepen, but not in my local html file. When I upload the very same file to my http server, it then works. No error in console.Genvieve
Does it makes sense to convert file entries into the actual file objects right away when they are iterated within getAllFileEntries, thus turning getAllFileEntries into a getAllFiles (after all, looks like that is the final target for most of use cases)? Or am I missing something?Shien
@Shien the main issue is that the File object does not enable us to readily handle the case where we have a directory and wish to get its files or sub-directories. This is the reason why we call dataTransferItemList[i].webkitGetAsEntry() instead of dataTransferItemList[i].getAsFile()Conger
@xlm, got it, thanks. Could you say, in your opinion, how heavy, performance-wise, the operation of converting a file entry into a File? I.e. if I have hundreds or thousands files, could it be a problem to convert them to File objects? Though I understand that to upload them, they are needed to be converted to File anyway...Shien
Converting to File doesn't appear to be intensive so I wouldn't worry about that operation. I've got this code in production and it easily handles tens of thousands of files. In fact my test harness was 40,000 files arbitrarily nested. Uploading of the content itself will of course depend on file size, disk, network etc.Conger
I've faced interesting behaviour I cannot understand. I'm using react-dropzone which internally uses file-selector package (I'll call it ORIGINAL). Unfortunately it does not handle empty folders, so I started to play with its fork github.com/Nerlin/file-selector (lets say FORKED). It appears that after I do reading with ORIGINAL file-selector, I cannot redo reading with FORKED one using the same dataTransfer.items. Somehow items are modified after read (but I do not see any changes in ORIGINAL codebase) as item.kind appears empty string and calling webkitGetAsEntry returns null.Oust
Does smb know why reading files and directories cannot be done twice?Oust
E
26

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

Erleneerlewine answered 5/7, 2017 at 6:45 Comment(6)
This should be the new accepted answer. It is better than other answers because it returns a promise when complete. But there were a few mistakes: function getFilesWebkitDataTransferItems(dataTransfer) should be function getFilesWebkitDataTransferItems(items), and for (entr of entries) should be for (let entr of entries).Penumbra
Won't actually get all the files in a directory (for Chrome it will only return 100 entries in a directory). Spec stipulates the need to call readEntries repeatedly until it returns an empty array.Conger
@Conger Updated npm package. Now it handles >100 entries.Erleneerlewine
Very helpful! Thanks for the solution. So far this is the most precise and clean one. This should be new accepted answer, I agree.Eartha
This is perfect! Thanks thanks thanks!Maciemaciel
Is it not possible to update the input.files with a FileList from the retrieved file array? I'm trying but it doesn't work.Dermatology
T
14

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

HTML5 now has 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.

Thrill answered 28/8, 2010 at 23:54 Comment(3)
Hmmm, I agree on point 2... but only as long as there is a way for the web developer to determine if they want to enable the upload of hidden files - as there is always the potential that a hidden file could be operative to the use of the uploaded folder. Especially if the folder is a full on document split into multiple parts like a final cut file might be.Militarism
Disagree with out of scope: this is a cause of incompatibilities for something many people want to do, so it should be specified.Gelinas
@CiroSantilliOurBigBook.com it's coming/here for chrome & safariOdom
I
10

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

Ingenerate answered 24/7, 2012 at 12:24 Comment(1)
Is it possible to do the same for downloading without using compressed folders ?Serb
A
8

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.

Allaround answered 19/11, 2016 at 16:47 Comment(0)
P
4

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

Prelusive answered 14/9, 2018 at 13:23 Comment(1)
Working great. Ported to Vue jsfiddle.net/KimNyholm/xua9kLnyWaldon
B
1

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

Benignity answered 12/6, 2012 at 4:42 Comment(1)
@MoB: maybe it is some kind of pointer indeed. But since the actual file is on the client machine, the server machine will not be able to do anything with this pointer, of course.Benignity
W
1

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

Washin answered 3/2, 2016 at 13:25 Comment(4)
Wow. Telling from the W3C Note at that link, this is indeed not continued. What is the basis of the assumption that it has failed to get any traction?Lillia
@Lillia no other browser vendors implemented itWashin
@PabloBarríaUrenda comment is not true; his issue is likely referring to his question: #51850969 which he solved/realised readEntries can't be called if another call of readEntries is still being run. The DirectoryReader API design isn't the bestConger
@Conger yes, indeed you are correct. I had posted this while I myself was puzzled by the issue, but I eventually resolved it (and forgot about this comment). I have now deleted the confusing comment.Retrogress
D
1

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

Dias answered 21/4, 2020 at 9:3 Comment(0)
M
0

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;

}

Maciemaciel answered 10/3, 2022 at 15:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.