0

Hi!

I want to upload multiple files with javascript / jquery.

My backend is an esp32.

Current code is working with only one file, but fails to upload multiple files. It is uploading just half of the files when multiple are in the array.

Here is my code:

var promises = [];
//Gets called onchange
function dragAndDrop(el){
    promises = [];
    // Get all the files from the input element
    var files = $(el).prop('files');
    // Make sure there are files, but it's onchange so it must be.
    if(files){
        // add spinner icon to indicate the upload start to the user
        $("#uploadIndicatorIcon").html( '<i class="fas fa-spinner fa-spin"></i>' );
        // Check if the files fit in the filesystem
        if( !isFileArrFits(files) ){
            debug("error","There is not enough space for the files!");
            uploadingDone();
            return false;
        }
        // iterate trought all the files and upload them one by one.
        for (let file of files) {
            promises.push( uploadFile(file) );
        }
        // register the promises done from the ajax.
        updatePromises();
    }
}

function updatePromises(){
    $.when.apply(null, promises).done(function() {
        if(promises.length > 1){
            $('.file-input').prev().text("Files uploaded.");
            debug("success","Files uploaded.");
        }else{
            $('.file-input').prev().text("File uploaded.");
        }
        setTimeout(() => {uploadingDone();}, 100);
    })
}

// reset everything
function uploadingDone(){
    $("#fileUploadInput").val("");
    $("#uploadIndicatorIcon").empty();
    $('.file-input').prev().text("or drag and drop multiple files or a firmware");
}

function uploadFile(file){
    var url = "/uploading";
    if( file.name.includes("firmware") ){url = "/uploadingfirm"}
    form = new FormData();
    form.append("file", file, file.name);
    return $.ajax({
        url: url,
        type: 'POST',
        data: form,
        processData: false,
        contentType: false,
        async: true
    }).done(function(resp){
        if( file.name.includes("firmware") ){
            debug("success","Firmware uploading success. The module is restarting...");
            setTimeout(() => {location.reload();}, 6500);
        }else{
            debug("success",`Uploaded: ${file.name}`);
        }
    }).fail(function(resp){
        debug("error",`Failed: ${file.name}`);
    });
}

I think the problem lies in here:

for (let file of files) {
  promises.push( uploadFile(file) );
}
updatePromises();

Since the esp32 is not capable to do a task in that sort amount of time, i tried to delay the requests like this:

for (let file of files) {
  setTimeout(() => {
    promises.push( uploadFile(file) );
  }, 100); // tried also with 200,500 and 1000ms.
}
updatePromises();

I thought that the updatePromises() function is the problem because it's gets called before the promises array fills up. So i did this:

var filesIndex = 0;
for (let file of files) {
  setTimeout(() => {
    filesIndex++;
    promises.push( uploadFile(file) );
    if(filesIndex >= files.length){
       updatePromises();
    }
  }, 100); // tried also with 200,500 and 1000ms.
}

Again, no success. This is probably because the files are starting to upload before i register a callback for the promises. Interestingly the updatePromises function get's called in every case but the files are just half way there always.

If i uploading just one file with this exact same method everything works fine.

So the question is.

How can i delay the uploading ajax? Should i set up a variable to every file separatelly to indicate if it is uploaded and upload the next one?

*EDIT: Here is the working code with the accepted solution:

function fileDragN_DropEvents(){
    var $fileInput = $('.file-input');
    var $droparea = $('.file-drop-area');
    $fileInput.on('dragenter focus click', function() {
      $droparea.addClass('is-active');
    });
    $fileInput.on('dragleave blur drop', function() {
      $droparea.removeClass('is-active');
    });
    $fileInput.on('change', function() {
      var filesCount = $(this)[0].files.length;
      var $textContainer = $(this).prev();
    
      if (filesCount === 1) {
        var fileName = $(this).val().split('\\').pop();
        $textContainer.text(fileName);
      } else {
        $textContainer.text(filesCount + ' files selected');
      }
    });
}

var promises = [];
function updatePromises() {
    $.when.apply(null, promises).done(function() {
        if(promises.length > 1){
            $('.file-input').prev().text("Files uploaded");
            debug("success","Files uploaded");
        }else{
            $('.file-input').prev().text("File uploaded");
        }
        setTimeout(() => {uploadingDone();}, 500);
    })
}

function uploadingDone(){
    $("#fileUploadInput").val("");
    $("#uploadIndicatorIcon").empty();
    $('.file-input').prev().text("or drag & drop multiple files or a firmware");
}

function dragAndDrop(el){
    promises = [];
    var files = $(el).prop('files');
    var promise = $.Deferred();
    promise.resolve();

    if( !isFileArrFits(files) ){
        debug("error","Files does not fit into the filesystem");
        uploadingDone();
        return false;
    }
    $("#uploadIndicatorIcon").html( '<i class="fas fa-spinner fa-spin"></i>' );

    for (let file of files) {
        promise = promise.then( () => uploadFile(file) );
        promises.push(promise);
    }
    updatePromises();
}

function uploadFile(file){
    var url = "/uploading";
    if( file.name.includes("firmware") ){url = "/uploadingfirm"}
    form = new FormData();
    form.append("file", file, file.name);
    return $.ajax({
        url: url,
        type: 'POST',
        data: form,
        processData: false,
        contentType: false,
        async: true
    }).done(function(resp){
        if( file.name.includes("firmware") ){
            debug("success","Firmware uploaded. Module restarts..");
            setTimeout(() => {location.reload();}, 6500);
        }else{
            debug("success",`Uploaded: ${file.name}`);
        }
    }).fail(function(resp){
        debug("error",`Failed: ${file.name}`);
    });
}
Dr.Random
  • 430
  • 3
  • 16
  • chain the promises rather than having them execute concurrently ... easy with regular Promises - jQuery defer on the other hand is a pain to work with at all – Jaromanda X Jun 29 '21 at 12:54
  • Can i chain the promises inside a for loop? I don't know how many files will be uploaded. – Dr.Random Jun 29 '21 at 12:57
  • Something like this? for (let file of files) { promises.push( new Promise((resolve, reject) => { uploadFile(file); }) ); } updatePromises(); function updatePromises(){ for (const promise of promises) { promise.then((result) => { $('.file-input').prev().text("File uploaded."); }); } } – Dr.Random Jun 29 '21 at 13:03
  • I can't read code in a comment - see answer - uses jQuery Deffered exclusively, like your current code – Jaromanda X Jun 29 '21 at 13:11

1 Answers1

0

here's one that uses ONLY $.Deferred

Personally I'd NEVER use $.Deffered, but, this was a challenge and I think I met it

// below this is just placehodler
var files = [1, 2, 3, 4, 5];

function uploadFile(v) {
  var p = $.Deferred();
  console.log(`${v} starting`);
  setTimeout(p.resolve, Math.random() * 1000 + 500, v);
  return p;
}

function updatePromises() {
  $.when.apply(null, promises).done(function() {
    console.log(arguments);
  })
}
var promises = [];
// above this is just placeholder
// this is the change
var promise = $.Deferred();
promise.resolve();
for (let file of files) {
  promise = promise.then(() => uploadFile(file)
    // not required, just shows sequence of events
    .then(r => {
      console.log(`${r} finished`);
      return `${r} done`;
    })
    // end of unrequired code
  );
  promises.push(promise);
}
// end of changes - the line below is the same as yours
updatePromises()
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
Jaromanda X
  • 53,868
  • 5
  • 73
  • 87
  • Wow. It's a little confusing at first sight but i will try to integrate it to my example and i will report back. – Dr.Random Jun 29 '21 at 13:17
  • Should i return the ajax request's promise in the uploadFile(v) function instead of the p? – Dr.Random Jun 29 '21 at 13:21
  • yes, the code in my `uploadFile` is just a place holder - don't change your `uploadFile` at all - I've added some comments to make it clearer – Jaromanda X Jun 29 '21 at 13:22
  • Thank you. I don't know what is the difference yet, but it is working. Really appreciate your help. – Dr.Random Jun 29 '21 at 13:33
  • the difference is, that the promises don't all run at once (thought that obvious with the console output) they run one after the other ... therefore, there's only ONE upload in flight at any one time – Jaromanda X Jun 29 '21 at 13:34
  • Okay, i can see that. What is promise.resolve() do? Can i simplify this: for (let file of files) { promise = promise.then( () => uploadFile(file) ); promises.push(promise); } to this: for (let file of files) { promises.push( promise.then( () => uploadFile(file) ) ); } – Dr.Random Jun 29 '21 at 13:39
  • `promise.resolve()` resolves the "initial" Deferred that is used to "start" the chain – Jaromanda X Jun 29 '21 at 13:42
  • Okay, i don't bother you anymore. I'm really glad. Thank you once again. – Dr.Random Jun 29 '21 at 13:43
  • oh ... no, you can't change it as you suggested ... that would just make everything run all at once again ... this code chains the promises (deferreds) one after the other – Jaromanda X Jun 29 '21 at 13:44