How to handle drag-and-drop directory uploads with phx-drop-target

Hey Elixir Forum, this is my first post.

I am trying to use phx-drop-target to allow users to drag and drop entire directories. My setup works fine when clicking the file input, but it fails when dragging and dropping a folder.

Here is my input setup:

<.live_file_input upload={@uploads.folder_uploads} webkitdirectory />

After looking into the LiveView source code for the drop event, it appears it only looks at dataTransfer.files. From what I could tell, directories show up, but only as 0 byte files with no contents.

I was able to around this issue by creating a hook to intercept the drop event, but it feels quite hacky.

Here is my current hook:

Drop = {
  mounted() {
    this.inputEl = document.getElementById(this.el.dataset.inputId);

    this.handleDrop = async (e) => {
      e.preventDefault();
      e.stopPropagation(); 

      const items = e.dataTransfer.items;
      const fileArray = [];
      const queue = [];

      for (const item of items) {
        const entry = item.webkitGetAsEntry();
        if (entry) {
          queue.push(this.traverseEntry(entry, fileArray));
        }
      }

      await Promise.all(queue);

      const dataTransfer = new DataTransfer();
      fileArray.forEach(file => dataTransfer.items.add(file));
      this.inputEl.files = dataTransfer.files;

      this.inputEl.dispatchEvent(new Event("input", { bubbles: true }));
      this.inputEl.dispatchEvent(new Event("change", { bubbles: true }));
    };

    this.el.addEventListener("drop", this.handleDrop, true);
  },

  destroyed() {
    this.el.removeEventListener("drop", this.handleDrop, true);
  },

  traverseEntry(entry, fileList, path = "") {
    return new Promise((resolve) => {
      if (entry.isFile) {
        entry.file((file) => {
          Object.defineProperty(file, "webkitRelativePath", {
            value: path + file.name,
            writable: false
          });
          fileList.push(file);
          resolve();
        });
      } else if (entry.isDirectory) {
        const dirReader = entry.createReader();
        const dirPath = path + entry.name + "/";
        
        const readBatch = () => {
          dirReader.readEntries(async (entries) => {
            if (entries.length === 0) {
              resolve();
            } else {
              const childPromises = entries.map(child => this.traverseEntry(child, fileList, dirPath));
              await Promise.all(childPromises);
              readBatch();
            }
          });
        };
        readBatch();
      } else {
        resolve();
      }
    });
  }
};

Is there a more native way to handle dropped directories that I missed?

Thanks.

3 Likes

Not sure about hacks/workarounds or maybe some very latest APIs available in dev builds, but generally speaking vendors made many approaches into directory upload and as far as I know no one has been standardised or is going to be in the nearest future - at least not within HTML5. I may be wrong here, so let’s see what others have to say. :thinking:

However if right now I would have to implement something like that then I would rather ask user to zip such a directory and send it as a file. That’s definitely the simplest possible way and of course well supported. Handling .zip file should also not be a big trouble especially if you are willing to use external tools. :toolbox:

There are many potential problems with directory API or rather file system API. Symbolic links or shortcuts, .. (parent directory), . (current directory) - there are so many places where things could go wrong. Of course it’s very easy to support them - that’s why vendors made many attempts already. However doing that for a cross-platform browser in optimal and secure way may be very challenging. :nerd_face:

Some people may say that we don’t have to support platform-specific navigation, but it can just break lots of things (such as library symlinks) or they are handy and would need to be re-implemented by hand again across many platforms and intended to be fast and secure at the same time. Commonly used feature is .. (parent directory) link in directory listings. Just one typo or mistake could cause millions of devices completely vulnerable. :unlocked:

Do you know the origin of --preserve-root option for rm command on Linux? If remember correctly wine developers made literally just one typo - they have added only 1 space character. Therefore instead of removing some game/settings directory they have removed root / and tried to remove not existing relative path (i.e. rest of the absolute path). :scream:

# bye, bye world!
rm -fr / path/to/game/lib

That’s most probably why you can find many plain sites with (server) directory listing rendered by rather a well secured backend. I never heard about an idea around directory/file system API that survived long enough to be commonly used or maybe it’s just me who is not using it. :sweat_smile:

2 Likes

I think directory enumeration on drop started around 2013 in Chrome, it’s not a new feature.

It’s now considered as a baseline (available in all major browsers) feature as of 2025 even if it keeps a few “webkit” prefixes because of the original implementation starting in Chrome first. Your implementation looks correct based on a quick MDN / SO search.

The original poster does not ask for server-side directory traversals but just uploading the content of a full folder, from the client, which is supported by browsers.

1 Like