valor-software / ng2-file-upload

Easy to use Angular components for files upload
http://valor-software.github.io/ng2-file-upload/
MIT License
1.91k stars 662 forks source link

Upload multiple files in one request #671

Open Salma7amed opened 7 years ago

Salma7amed commented 7 years ago

What I noticed is that if I upload multiple files at once. The uploader performs multiple requests to the url for each single file. So is it possible to receive all the files in one request ?

vijj commented 7 years ago

multi fileupload by using flowJs multiple request fine depends on fileFize. can any one ng2-file-upload problem on multiplefile.

rbasniak commented 7 years ago

I noticed this too, the .uploadAll() method does a single request for every file in queue.

@Salma7amed Did have any sucess on uploading all files in a single request?

Salma7amed commented 7 years ago

@rbasniak not using this library.

rbasniak commented 7 years ago

@Salma7amed What library are you using now?

Salma7amed commented 7 years ago

@rbasniak I used FormData to perform a post request with the files.

ClaytonBrawley commented 7 years ago

The file that uploads the attached files in ngx-uploader.ts has a function that it uses called uploadFilesInQueue(). What this function does is loop through the queue and sends each file off to uploadFile() to be sent off using a XMLHttpRequest. What you can do to send all files at one time is, instead of calling uploadFile() for each file in the queue, add each file to the FormData form before sending it off. Something like this:

uploadFilesInQueue(): void {
    if (this.getQueueSize() === 1) {
      this.uploadFile(this._queue[0]);
    } else if (this.getQueueSize() > 1) {
      this.uploadAllFiles();
    }
  }

uploadAllFiles(): void {
    const xhr = new XMLHttpRequest();
    const form = new FormData();

    for (const file of this._queue) {
      form.append(this.opts.fieldName, file, file.name);
    }

    xhr.send(form);
  };

Just like @Salma7amed said above.

josecarlosaparicio commented 7 years ago

Finally, I was inspired thanks to @ClaytonBrawley and can solve this issue extending FileUploader class:

import { FileUploader, FileItem, FileUploaderOptions } from 'ng2-file-upload';

export class FileUploaderCustom extends FileUploader {

  constructor(options: FileUploaderOptions) {
    super(options);
  }

  uploadAllFiles(): void {

    var xhr = new XMLHttpRequest();
    var sendable = new FormData();
    var fakeitem: FileItem = null;
    this.onBuildItemForm(fakeitem, sendable);

    for (const item of this.queue) {
      item.isReady = true;
      item.isUploading = true;
      item.isUploaded = false;
      item.isSuccess = false;
      item.isCancel = false;
      item.isError = false;
      item.progress = 0;

      if (typeof item._file.size !== 'number') {
        throw new TypeError('The file specified is no longer valid');
      }
      sendable.append("files", item._file, item.file.name);
    }

    if (this.options.additionalParameter !== undefined) {
      Object.keys(this.options.additionalParameter).forEach((key) => {
        sendable.append(key, this.options.additionalParameter[key]);
      });
    }

    xhr.onload = () => {
      var gist = (xhr.status >= 200 && xhr.status < 300) || xhr.status === 304 ? 'Success' : 'Error';
      var method = 'on' + gist + 'Item';
      this[method](fakeitem, null, xhr.status, null);

    };
    xhr.onerror = () => {
      this.onErrorItem(fakeitem, null, xhr.status, null);
    };

    xhr.onabort = () => {
      this.onErrorItem(fakeitem, null, xhr.status, null);
    };

    xhr.open("POST", this.options.url, true);
    xhr.withCredentials = true;
    if (this.options.headers) {
      for (var _i = 0, _a = this.options.headers; _i < _a.length; _i++) {
        var header = _a[_i];
        xhr.setRequestHeader(header.name, header.value);
      }
    }
    if (this.authToken) {
      xhr.setRequestHeader(this.authTokenHeader, this.authToken);
    }
    xhr.send(sendable);
  };

}

Then, in the component can be use like this:

uploader: FileUploaderCustom;

ngOnInit() {
    this.uploader = new FileUploaderCustom ({
        url: urlSubirMiniatura
    });
}

uploadAllFiles(){
   this.uploader.uploadAll();
}

Notice that all files are appending to sendable with the name "files", so it could be refactored better.

varrob112 commented 7 years ago

@josecarlosaparicio : uploadAllFiles(){ this.uploader.uploadAll(); } You mean this.uploader.uploadAllFiles(); ?

dannyhchan commented 7 years ago

@josecarlosaparicio : onSuccess does not read the response if server is going to return a json object. Any way to fix this?

Max053 commented 7 years ago

Is this 1 file per request the expected behaviour from the beginning, or would a PR be accepted if it were to fix this behaviour (maybe with an optional setter)?

dannyhchan commented 7 years ago

1 file per request is working as expected. I think with an option setter for upload all rather than per file would be good. What I have as a workaround is all on the server end by submitting the total queue count and along with the files and handling it with the 1 file per upload scenario. If the entire batch of files can be submitted at one, this could be avoided.

judsonmusic commented 7 years ago

Big TIME +1 I love the plugin but its steers away from server performance by doing multiple requests. I would love to be able to send all of the file in an array at once and loop through the file array files.map(obj)=>{ fs.writeFIle... }

I am not a big fan of extending(hacking) other people's work, "Patience is a Virtue" :)

Max053 commented 7 years ago

@judsonmusic I'm currently working on this feature, I will open up a PR as soon as it's finished :)

adrianfaciu commented 7 years ago

We're trying to get this into a better shape so if you still have that PR it would be awesome 😉

ClaytonBrawley commented 7 years ago

If no one gets to this I can try and throw something together if more details are provided.

adrianfaciu commented 7 years ago

@ClaytonBrawley you can assign this to yourself and see what you can done. Once you have something create a PR and we can all have a look.

andrew-starosciak commented 7 years ago

@dannyhchan See below for a workaround on firing the onCompleteItem callback.

import {FileItem, FileUploader, FileUploaderOptions} from 'ng2-file-upload';

export class FileUploaderCustom extends FileUploader {

    constructor(
        options: FileUploaderOptions
    ) {
        super(options);
    }

    uploadAllFiles(): void {
        // const _this = this;
        const xhr = new XMLHttpRequest();
        const sendable = new FormData();
        const fakeItem: FileItem = null;
        this.onBuildItemForm(fakeItem, sendable);

        for (const item of this.queue) {
            item.isReady = true;
            item.isUploading = true;
            item.isUploaded = false;
            item.isSuccess = false;
            item.isCancel = false;
            item.isError = false;
            item.progress = 0;

            if (typeof item._file.size !== 'number') {
                throw new TypeError('The file specified is no longer valid');
            }
            sendable.append('files[]', item._file, item.file.name);
        }

        if (this.options.additionalParameter !== undefined) {
            Object.keys(this.options.additionalParameter).forEach((key) => {
                sendable.append(key, this.options.additionalParameter[key]);
            })
        }

        xhr.onerror = () => {
            this.onErrorItem(fakeItem, null, xhr.status, null);
        }

        xhr.onabort = () => {
            this.onErrorItem(fakeItem, null, xhr.status, null);
        }

        xhr.open('POST', this.options.url, true);
        xhr.withCredentials = true;
        if (this.options.headers) {
            for (let _i = 0, _a = this.options.headers; _i < _a.length; _i++) {
                const header = _a[_i];
                xhr.setRequestHeader(header.name, header.value);
            }
        }
        if (this.authToken) {
            xhr.setRequestHeader(this.authTokenHeader, this.authToken);
        }

        xhr.onload = () => {
            const headers = this._parseHeaders(xhr.getAllResponseHeaders());
            const response = this._transformResponse(xhr.response, headers);
            const gist = this._isSuccessCode(xhr.status) ? 'Success' : 'Error';
            const method = '_on' + gist + 'Item';
            for (const item of this.queue) {
                this[method](item, response, xhr.status, headers);
            }
            this._onCompleteItem(this.queue[0], response, xhr.status, headers);
        }

        xhr.send(sendable);
    }
}
drdreo commented 6 years ago

Ran into the same Issue. Ended up rewriting my process function on the server to handle one item per call and not all at once. Annoying because i can't bulk further requests.

a-morn commented 6 years ago

@adrianfaciu Is anyone working on this? I could do a PR otherwise

adrianfaciu commented 6 years ago

As far as I know, no, there is no open PR for this.

koenvanderlinden commented 6 years ago

@adrianfaciu @a-morn I create a PR #993 for a multiupload in one reqeust. It probably needs some refactoring. Let me know what you think of it.

eikishi01 commented 5 years ago

I was just looking for a way to upload all files individually and I got to this threat finding out that it is the default behaviour, which is what I needed @koenvanderlinden does you PR #993 consider allowing the use of both approaches? both use cases are valid and it should count for them.

koenvanderlinden commented 5 years ago

@eikishi01 you could use both. The way how multiple upload is done is based on configuration of the upload component.

TianrenWang commented 5 years ago

@andrew-starosciak Can you explain the reasoning behind doing a for loop on each item in the queue? With the for loop you receive multiple responses from the uploaded server, when only one response is necessary.

sridharan31 commented 4 years ago

this.uploader.clearQueue(); onsucess file not removed get error

mandateCancelComponent.html:786 ERROR TypeError: Cannot read property 'abort' of undefined
    at FileUploaderCustom.push../node_modules/ng2-file-upload/file-upload/file-uploader.class.js.FileUploader.cancelItem (file-uploader.class.js:112)
    at FileItem.push../node_modules/ng2-file-upload/file-upload/file-item.class.js.FileItem.cancel (file-item.class.js:38)
    at FileUploaderCustom.push../node_modules/ng2-file-upload/file-upload/file-uploader.class.js.FileUploader.removeFromQueue (file-uploader.class.js:85)
    at FileItem.push../node_modules/ng2-file-upload/file-upload/file-item.class.js.FileItem.remove (file-item.class.js:41)
    at Object.eval [as handleEvent] (mandateCancelComponent.html:795)
    at handleEvent (core.js:28969)
    at callWithDebugContext (core.js:30039)
    at Object.debugHandleEvent [as handleEvent] (core.js:29766)
    at dispatchEvent (core.js:19631)
    at core.js:28178
ghost commented 4 years ago

@andrew-starosciak

i think this works for multiple files :

xhr.onload = () => {
      const headers = this._parseHeaders(xhr.getAllResponseHeaders());
      const response = this._transformResponse(xhr.response, headers);
      const gist = this._isSuccessCode(xhr.status) ? "Success" : "Error";
      const method = "_on" + gist + "Item";
      const queueLength = this.queue.length;
      for (var i = 0; i < queueLength; i++) {
        this[method](
          this.queue[this.queue.length - 1],
          response,
          xhr.status,
          headers
        );
        this._onCompleteItem(
          this.queue[this.queue.length - 1],
          response,
          xhr.status,
          headers
       );
     }   
 };
costeacosmin92 commented 4 years ago

Hi everyone, I am using this plugin with Angular 9, and Spring Boot on backend, and was asked to create a questionnaire where for every item on the checklist the user can upload one or more pictures. My trouble was the same as the OP's, and ended up getting FileItem[] array from the 'uploader', and putting each file on a Zip file using JSZip library.

import { FileUploader, FileUploaderOptions, FileItem } from 'ng2-file-upload';
import * as JSZip from 'jszip';

async onFormSubmit() {
      let zipFile: JSZip = new JSZip();

      let items: FileItem[] = this.uploader.getNotUploadedItems().filter((item: FileItem) => 
           !item.isUploading);

      items.forEach(item  =>{
          zipFile.file(item.file.name, item.file.rawFile, {base64: true});
      })

      let finalZip = await zipFile.generateAsync({type:"blob", compression: "DEFLATE"});

     // now you can send 'finalZip' to backend using common HttpClient .
}

Hope it can be useful.

SohrabRo commented 2 years ago

@josecarlosaparicio your solution works like a charm but the progress bar is set to 0 and it is never updated! Therefore the progress bar does not work. Any solution about this problem you may think of?