ng2-file-upload icon indicating copy to clipboard operation
ng2-file-upload copied to clipboard

Upload multiple files in one request

Open Salma7amed opened this issue 7 years ago • 28 comments

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 ?

Salma7amed avatar Mar 14 '17 13:03 Salma7amed

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

vijj avatar Mar 24 '17 09:03 vijj

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?

rbasniak avatar Apr 02 '17 00:04 rbasniak

@rbasniak not using this library.

Salma7amed avatar Apr 05 '17 12:04 Salma7amed

@Salma7amed What library are you using now?

rbasniak avatar Apr 05 '17 12:04 rbasniak

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

Salma7amed avatar Apr 05 '17 19:04 Salma7amed

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.

ClaytonBrawley avatar Apr 06 '17 17:04 ClaytonBrawley

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.

josecarlosaparicio avatar Jul 06 '17 12:07 josecarlosaparicio

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

varrob112 avatar Jul 16 '17 07:07 varrob112

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

dannyhchan avatar Aug 10 '17 16:08 dannyhchan

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)?

Max053 avatar Aug 15 '17 11:08 Max053

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.

dannyhchan avatar Aug 15 '17 12:08 dannyhchan

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" :)

judsonmusic avatar Aug 18 '17 04:08 judsonmusic

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

Max053 avatar Aug 20 '17 15:08 Max053

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

adrianfaciu avatar Oct 02 '17 15:10 adrianfaciu

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

ClaytonBrawley avatar Oct 02 '17 16:10 ClaytonBrawley

@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.

adrianfaciu avatar Oct 02 '17 19:10 adrianfaciu

@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);
    }
}

andrew-starosciak avatar Oct 13 '17 17:10 andrew-starosciak

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.

drdreo avatar Nov 25 '17 14:11 drdreo

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

a-morn avatar Jan 24 '18 12:01 a-morn

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

adrianfaciu avatar Jan 24 '18 17:01 adrianfaciu

@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.

koenvanderlinden avatar Mar 17 '18 16:03 koenvanderlinden

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.

eikishi01 avatar Feb 11 '19 14:02 eikishi01

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

koenvanderlinden avatar Feb 12 '19 18:02 koenvanderlinden

@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.

TianrenWang avatar Jul 15 '19 14:07 TianrenWang

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

sridharan31 avatar Nov 14 '19 14:11 sridharan31

@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
       );
     }   
 };

ghost avatar Mar 30 '20 12:03 ghost

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.

costeacosmin92 avatar Apr 16 '20 16:04 costeacosmin92

@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?

SohrabRo avatar Dec 18 '21 10:12 SohrabRo