vaadin / web-components

A set of high-quality standards based web components for enterprise web applications. Part of Vaadin 20+
https://vaadin.com/docs/latest/components
470 stars 83 forks source link

[upload] Add support for uploading folders #857

Open onuridrisoglu opened 4 years ago

onuridrisoglu commented 4 years ago

It should be possible to select or drop a folder and upload the contents of the folder

Haprog commented 4 years ago

This is a good idea to offer a slightly better UX for uploading all files in a directory. Can be considered as progressive enhancement imo and ok to implement even if it won't work on all browsers (looks like it's well supported on desktop browsers except not on IE11).

Related resources:

Haprog commented 4 years ago

One thing to consider is what should happen if the selected directory contains nested directories with more files (and possibly files of same name in sub directories)? I didn't check if the native APIs allow for getting the whole directory hierarchy or only some flattened list of files from the directory (or does it even get files from sub directories of selected directory?).

There could be an additional (optional) related feature that would automatically zip the directory on client side before uploading it. Or other special handling (or customizable handler method) might be needed.

We should probably investigate how it behaves in these cases with the native:

<input type="file" directory>

and try to mimic that behaviour. Possibly providing additional value/features.

web-padawan commented 1 year ago

Note, we can use webkitdirectory attribute for uploading folders - see the Codepen illustrating how it works. While it allows to select folders, it disallows selecting individual files, so you can't have both at the same time.

web-padawan commented 1 year ago

Here's the current version of the File and Directory Entries API: https://wicg.github.io/entries-api/

It contains some examples of how to get the directories from the drag and drop (which will require us to change how we handle event.dataTransfer object) and helper functions e.g. getEntriesAsPromise() and readFileEntry().

These APIs could be used to get the contents (individual files) from the folder uploaded using drag & drop.

tommilukkarinen commented 1 year ago

Had some fun learning JS and injecting code into Vaadin client:

Problems: Does not pass folder structure to server, might be easiest to make with standard element.$server invocation

Test with text files, these will print to the screen

Tried only in Jetty, might be some problems in production release?

Use:

  1. press inject button
  2. drop folders to upload area
package fi.protieto.juuri.front.pages.map4;

import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.upload.Upload;
import com.vaadin.flow.component.upload.receivers.MultiFileMemoryBuffer;
import com.vaadin.flow.router.Route;
import org.apache.commons.io.IOUtils;

import java.io.InputStream;
import java.nio.charset.StandardCharsets;

// written on top of Vaadin autoupload example

@Route("upload-auto-upload-disabled")
public class AutoUpload extends Div {
    // event listener explained
    // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
    // handling folders explained
    // https://stackoverflow.com/questions/3590058/does-html5-allow-drag-drop-upload-of-folders-or-a-folder-tree
    // replacing functions explained:
    // https://stackoverflow.com/questions/2136522/can-you-alter-a-javascript-function-after-declaring-it
    // web component:
    // https://github.com/vaadin/web-components/blob/main/packages/upload/src/vaadin-upload.js
    // source in chrome:
    // vaadin -> bundles -> node-modules -> @vaadin -> upload -> src -> vaadin-upload.js

    public AutoUpload() {
        MultiFileMemoryBuffer buffer = new MultiFileMemoryBuffer();
        Upload upload = new Upload(buffer);
        add(upload);

        Button injectJS = new Button("inject");
        injectJS.addClickListener(e -> {
            upload.getElement().executeJs(
                            "" +

                            "function traverseFileTree(item, path, uploadThing) {" +
                                "if (item.isFile) {" +
                                    "item.file(function(file) {" +
                                        "uploadThing._addFile(file);" +
                                    "});" +
                                "}" +
                                "if (item.isDirectory) {" +
                                    "var dirReader = item.createReader();" +
                                    "dirReader.readEntries(function(entries) {" +
                                        "for (var i=0; i<entries.length; i++) {" +
                                            "traverseFileTree(entries[i], path + item.name + '/', uploadThing);" +
                                        "}" +
                                    "});" +
                                "}" +
                            "}" +

                            "this.addEventListener('drop', function(event) {" +
                                "var items = event.dataTransfer.items;" +
                                "console.log('_onDrop top level item size: ' + items.length);" +
                                "for (var i=0; i<items.length; i++) {" +
                                    "var item = items[i].webkitGetAsEntry();" +
                                    "var length = traverseFileTree(item, '', this);" +
                                "}" +
                            "});" +

                            "this._addFiles = function(files) {" +
                                "if(!files.isArray) {" +
                                    "console.log('_addFiles files as string (not array):' + JSON.stringify(files));" +
                                    "return;" +
                                "}" +
                            "};"
                    );
        });

        add(injectJS);

        upload.addSucceededListener(e -> {
            try {
                InputStream is = buffer.getInputStream(e.getFileName());

                String result = IOUtils.toString(is, StandardCharsets.UTF_8);

                System.out.println(e.getFileName() + ": " + result);

                is.close();
            }catch (Exception ex){
                ex.printStackTrace();
            }
        });

    }
}
rolfsmeds commented 1 month ago

If it's not possible to support both files and folders in the native file selector, then we'll just have to accept that, and document that switching to folder mode means you can only upload files thru drag and drop, and that if you need to support both through the native selector, you'll have to provide a toggle for switching between file and folder upload.

sissbruecker commented 3 weeks ago

Support for dragging and dropping folders has been added with https://github.com/vaadin/web-components/pull/8032.

For selecting a directory through the native file dialog there is a draft PR here: https://github.com/vaadin/web-components/pull/8057.

As the implementation involves some opinionated choices and browser limitations, we want to outline the planned implementation here in order to possibly get some feedback:

If you have any feedback regarding the proposed implementation please leave a comment.

jorgheymans commented 3 weeks ago

Interesting ! Did you consider some sort of upload queueing, so that uploading a directory with hundreds of large files does not swamp the server ?

sissbruecker commented 3 weeks ago

Nope, but that seems like a good idea. However I think this is a separate issue and that problem already exists today as you can select an unlimited number of files. So that will probably not make it into a first version of this feature.

knoobie commented 3 weeks ago

Mentioned issue: https://github.com/vaadin/web-components/issues/6698