Multi-File Upload Made Easy: How to Drag and Drop Directories in Your Web App

Are you tired of dragging and dropping files one by one like it's 1995? Well, fear not! In this tutorial, we're gonna make drag and drop work for directories like a charm, using nothing but a little bit of JavaScript and a dash of magic.

Disclaimer: "Please don't take this seriously, I'm just assuming all of my readers are React wizards who could create a React app using just their minds. So, I'll spare you the boredom of explaining how to use create-react-app."

First things first, let's define a dropzone element in your HTML where your users can drop their directories. This is where the magic happens! We'll use a div element and the ondrop event to make this happen:

import "./App.css";

function App() {
  const [className, setClassName] = useState("");
  const [results, setResults] = useState([]);

  return (
    <div className="App">
      <header className="App-header">
        <p>
          Drag and drop one or multiple files or directories into the rectangle
        </p>
        <div
          id="dropzone"
          className={className}
          onDragOver={onDragOver}
          onDragEnter={onDragEnter}
          onDragLeave={onDragLeave}
          onDrop={onDropItems}
        >
          Drop your directory here
        </div>

        {!!results.length &&
          results.map((result) => <div key={result.path}>{result.path}</div>)}
      </header>
    </div>
  );
}

export default App;

Pretty simple, right? The onDrop event is triggered when your user drops a file or directory onto the dropzone and the onDragOver event is triggered when your user drags a file or directory over the dropzone.

Now, let's add event listeners to the dropzone element to handle the drop and dragover events:

const supportsFileSystemAccessAPI =
  "getAsFileSystemHandle" in DataTransferItem.prototype;
const supportsWebkitGetAsEntry =
  "webkitGetAsEntry" in DataTransferItem.prototype;

const onDragOver = (e) => {
  e.preventDefault();
};

const onDragEnter = () => {
  setClassName("outline");
};

const onDragLeave = () => {
  setClassName("");
};

const onDropItems = async (e) => {
  // Prevent navigation.
  e.preventDefault();
  if (!supportsFileSystemAccessAPI && !supportsWebkitGetAsEntry) {
    // Cannot handle directories.
    return;
  }
  // Unhighlight the drop zone.
  setClassName("");

  const files = await getAllFileEntries(e.dataTransfer.items);
  const flattenFiles = files.reduce((acc, val) => acc.concat(val), []);
  console.log("Results here dude!!! : ", flattenFiles);
  setResults(flattenFiles);
};

const getAllFileEntries = async (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) {
      let reader = entry.createReader();
      queue.push(...(await readAllDirectoryEntries(reader)));
    }
  }
  // return fileEntries;
  return Promise.all(
    fileEntries.map((entry) => readEntryContentAsync(entry))
  );
};

// Get all the entries (files or sub-directories) in a directory by calling readEntries until it returns empty array
const readAllDirectoryEntries = async (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
const readEntriesPromise = async (directoryReader) => {
  try {
    return await new Promise((resolve, reject) => {
      directoryReader.readEntries(resolve, reject);
    });
  } catch (err) {
    console.error(err);
  }
};

const readEntryContentAsync = async (entry) => {
  return new Promise((resolve, reject) => {
    let reading = 0;
    const contents = [];

    reading++;
    entry.file(async (file) => {
      reading--;
      const rawFile = file;
      rawFile.path = entry.fullPath;
      contents.push(rawFile);

      if (reading === 0) {
        resolve(contents);
      }
    });
  });
};

And voila! You can now drag and drop directories like a boss. But wait, what's this FileSystemDirectoryHandle API? It's a shiny new API that's still in the experimental stage, but it allows us to work with files and directories in a more modern and efficient way. However, not all browsers support it just yet, so it's always a good idea to include fallbacks for browsers that don't support it.

That's it for this tutorial. I hope you've found it useful and entertaining! Now go forth and drag and drop with reckless abandon!


The repository with all code is here: https://github.com/mike-yuen/multi-file-upload

Buy Me a Coffee

if you find this article helpful 🧑‍💻

Thanks for reading Mike's Blog!
Subscribe for free to receive new posts and support our work.

I won't send you spam.
Unsubscribe at any time.