Search code examples
javascriptnode.jsexpressdrag-and-dropmulter

Uploading a file with FormData and multer


I have successfully managed to upload a file to a Node server using the multer module by selecting the file using the input file dialog and then by submitting the form, but now I would need, instead of submitting the form, to create a FormData object, and send the file using XMLHttpRequest, but it isn't working, the file is always undefined at the server-side (router).

The function that does the AJAX request is:

function uploadFile(fileToUpload, url) {

  var form_data = new FormData();

  form_data.append('track', fileToUpload, fileToUpload.name);

  // This function simply creates an XMLHttpRequest object
  // Opens the connection and sends form_data
  doJSONRequest("POST", "/tracks/upload", null, form_data, function(d) {
    console.log(d);
  })

}

Note that fileToUpload is defined and the url is correct, since the correct router method is called. fileToUpload is a File object obtained by dropping a file from the filesystem to a dropzone, and then by accessing the dataTransfer property of the drop event.

doJSONRequest is a function that creates a XMLHttpRequest object and sends the file, etc (as explained in the comments).

function doJSONRequest(method, url, headers, data, callback){

  //all the arguments are mandatory
  if(arguments.length != 5) {
    throw new Error('Illegal argument count');
  }

  doRequestChecks(method, true, data);

  //create an ajax request
  var r = new XMLHttpRequest();

  //open a connection to the server using method on the url API
  r.open(method, url, true);

  //set the headers
  doRequestSetHeaders(r, method, headers);

  //wait for the response from the server
  r.onreadystatechange = function () {
    //correctly handle the errors based on the HTTP status returned by the called API
    if (r.readyState != 4 || (r.status != 200 && r.status != 201 && r.status != 204)){
      return;
    } else {
      if(isJSON(r.responseText))
        callback(JSON.parse(r.responseText));
      else if (callback !== null)
        callback();
    }
  };

  //set the data
  var dataToSend = null;
  if (!("undefined" == typeof data) 
    && !(data === null))
    dataToSend = JSON.stringify(data);

  //console.log(dataToSend)

  //send the request to the server
  r.send(dataToSend);
}

And here's doRequestSetHeaders:

function doRequestSetHeaders(r, method, headers){

  //set the default JSON header according to the method parameter
  r.setRequestHeader("Accept", "application/json");

  if(method === "POST" || method === "PUT"){
    r.setRequestHeader("Content-Type", "application/json");
  }

  //set the additional headers
  if (!("undefined" == typeof headers) 
    && !(headers === null)){

    for(header in headers){
      //console.log("Set: " + header + ': '+ headers[header]);
      r.setRequestHeader(header, headers[header]);
    }

  }
}

and my router to upload files is the as follows

// Code to manage upload of tracks
var multer = require('multer');
var uploadFolder = path.resolve(__dirname, "../../public/tracks_folder");

function validTrackFormat(trackMimeType) {
  // we could possibly accept other mimetypes...
  var mimetypes = ["audio/mp3"];
  return mimetypes.indexOf(trackMimeType) > -1;
}

function trackFileFilter(req, file, cb) {
  cb(null, validTrackFormat(file.mimetype));
}

var trackStorage = multer.diskStorage({
  // used to determine within which folder the uploaded files should be stored.
  destination: function(req, file, callback) {

    callback(null, uploadFolder);
  },

  filename: function(req, file, callback) {
    // req.body.name should contain the name of track
    callback(null, file.originalname);
  }
});

var upload = multer({
  storage: trackStorage,
  fileFilter: trackFileFilter
});


router.post('/upload', upload.single("track"), function(req, res) {
  console.log("Uploaded file: ", req.file); // Now it gives me undefined using Ajax!
  res.redirect("/"); // or /#trackuploader
});

My guess is that multer is not understanding that fileToUpload is a file with name track (isn't it?), i.e. the middleware upload.single("track") is not working/parsing properly or nothing, or maybe it simply does not work with FormData, in that case it would be a mess. What would be the alternatives by keeping using multer?

How can I upload a file using AJAX and multer?

Don't hesitate to ask if you need more details.


Solution

  • multer uses multipart/form-data content-type requests for uploading files. Removing this bit from your doRequestSetHeaders function should fix your problem:

    if(method === "POST" || method === "PUT"){
       r.setRequestHeader("Content-Type", "application/json");
    }
    

    You don't need to specify the content-type since FormData objects already use the right encoding type. From the docs:

    The transmitted data is in the same format that the form's submit() method would use to send the data if the form's encoding type were set to multipart/form-data.

    Here's a working example. It assumes there's a dropzone with the id drop-zone and an upload button with an id of upload-button:

    var dropArea  = document.getElementById("drop-zone");
    var uploadBtn = document.getElementById("upload-button");
    var files     = [];
    
    uploadBtn.disabled = true;
    uploadBtn.addEventListener("click", onUploadClick, false);
    
    dropArea.addEventListener("dragenter", prevent, false);
    dropArea.addEventListener("dragover",  prevent, false);
    dropArea.addEventListener("drop", onFilesDropped, false);   
    
    //----------------------------------------------------
    function prevent(e){
    
        e.stopPropagation();
        e.preventDefault();
    }
    
    //----------------------------------------------------
    function onFilesDropped(e){
    
        prevent(e);
    
        files = e.dataTransfer.files;
    
        if (files.length){
            uploadBtn.disabled = false;
        }
    }
    
    //----------------------------------------------------
    function onUploadClick(e){
    
        if (files.length){
            sendFile(files[0]);
        }
    }
    
    //----------------------------------------------------
    function sendFile(file){
    
        var formData = new FormData();
        var xhr      = new XMLHttpRequest();
    
        formData.append("track", file, file.name);
    
        xhr.open("POST", "http://localhost:3000/tracks/upload", true);
    
        xhr.onreadystatechange = function () {  
            if (xhr.readyState === 4) {  
                if (xhr.status === 200) {  
                    console.log(xhr.responseText);
                } else {  
                    console.error(xhr.statusText);  
                }  
            }  
        };
    
        xhr.send(formData);
    }
    

    The server side code is a simple express app with the exact router code you provided.