Uploading a file with FormData and multer - javascript

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.

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.

to post a FormData object accepted by multer the upload function should be like this:
function uploadFile(fileToUpload, url) {
var formData = new FormData();
//append file here
formData.append('file', fileToUpload, fileToUpload.name);
//and append the other fields as an object here
/* var user = {name: 'name from the form',
email: 'email from the form'
etc...
}*/
formData.append('user', user);
// This function simply creates an XMLHttpRequest object
// Opens the connection and sends form_data
doJSONRequest("POST", "/tracks/upload", null, formData, function(d) {
console.log(d);
})
}

Related

Uploading file with FormData and XmlHttpRequest with Formidable on node js backend shows empty files, fields

I am uploading a csv file using FormData and XmlHttpRequest. Here is the code for that.
I have a form wrapped around an html input type file, whose onchange event I am executing this code. I have tried to send the form directly as well and also read the form element into the FormData object.
let formData = new FormData();
let file = e.target.files[0];
var blob = new Blob([file],{type: 'text/csv'});
formData.append("payoutUpload", blob, 'processed.csv');
let uri = encodeURI(`${window.serviceUri}${path}`);
var req = new XMLHttpRequest();
req.onload = (result) => {
if (req.status === 500 && result && result.code === 'ECONNRESET') {
console.log(
'Connection was reset, hence retry the sendRequest function'
);
} else if (req.status === 200) {
} else {
console.log("Error while retrieving data");
}
}
req.onerror = (e) => {
console.log('There was an error while retrieving data from service', e);
};
req.open('POST', uri, true);
req.setRequestHeader('Content-Type', 'multipart/form-data');
req.setRequestHeader('Authorization', 'Bearer ' + token);
req.send(formData);
When I send the request, I can see that the file is being sent in the form of Request Payload.
On the NodeJs backend, I am running Express and Formidable. I am not using body-parser, I am using express's inbuilt json and urlencoding methods.
Here is the formidable part.
const form = formidable({multiples: true});
form.parse(req, (err, fields, files) => {
console.log(`error is ${JSON.stringify(err)}`);
console.log(`fields is ${JSON.stringify(fields)}`);
console.log(`files JSON: ${JSON.stringify(files)}`);
console.log('file in request: ' + files.payoutUpload);
console.log(`req.body: ${req.body}`);
options.file = files.payoutUpload;
});
I get err, fields and files as empty. I have searched through all similar questions and set the request headers correctly(which is usually the issue). I can see that the request.body still has the file payload on the server end. But formidable does not parse this. Can anyone tell what I am doing wrong?
UPDATE: I have tried other packages for parsing the file, like multer, express-fileupload, all of them return files as empty. I have also tried fetch API to send my request, but with no luck.
req.setRequestHeader('Content-Type', 'multipart/form-data')
When you send multipart/form-data you must include a boundary parameter in the header however you can't know what value you need to set for this.
Don't set the Content-Type header at all. Allow XMLHttpRequest to generate it automatically from the FormData object.

How to know if XMLHttpRequest has finished?

I'm uploading multiple files with form field
<input type="file" accept="image/jpeg, image/png" multiple
a loop sends all files to upload function
// Upload photo function
var upload = function (photo, callback) {
var formData = new FormData();
formData.append('photo', photo);
var request = new XMLHttpRequest();
request.onreadystatechange = function() {
if (request.readyState === 4) {
callback(request.response);
}
}
request.open('POST', 'upload.php');
request.responseType = 'json';
request.send(formData);
};
for (var i = 0; i < files.length; i++) {
upload(resizedFile, function (response) { });
}
if all files are successfully uploaded, want to redirect ..
//all files finished, go to ...
if(response !== null && typeof response !== 'object') var response = JSON.parse(response);
window.location.href = "my-new-url.php?id="+response.id;
my problem, the redirect is done before all uploads are finished. how can I make the script wait for all xmlhttprequests to finish?
EDIT
I changed the upload function like this, to upload with jQuery. The fuction is called several times from the for loop, so how can I wait for all loops/jquery calls are finished, then redirect?
function upload (photo, callback) {
var fd = new FormData();
fd.append('photo',photo);
return $.ajax({
url: 'upload.php',
type: 'post',
data: fd,
contentType: false,
processData: false,
success: function(response){
},
});
};
You can increment a global counter variable in the callback. When it reaches the length of the array, all the requests have completed.
var completed = 0;
for (var i = 0; i < files.length; i++) {
upload(resizedFile, function (response) {
if (++completed == files.length) {
window.location = redirect_url;
}
});
}
First of all, I would promisify the upload function. With JQuery would be much shorter as $.ajax returns a thenable object, but with old-fashioned vanilla JS, it would look like this:
var upload = function (photo) {
return new Promise(function(resolve, reject) {
var request = new XMLHttpRequest();
request.onload = function() {
if (request.status >= 200 && request.status < 300) {
resolve(request.response);
} else {
reject(request.statusText);
}
};
request.onerror = function() { reject(request.statusText) }
request.open('POST', 'upload.php');
request.responseType = 'json';
var formData = new FormData();
formData.append('photo', photo);
request.send(formData);
})
}
See also: http://ccoenraets.github.io/es6-tutorial-data/promisify/
Then I would collect these promises in an array:
var uploads = files.map(function(file) { return upload(resizeFile(file)); })
(or simply iterate as you did and push them in an array)
Then you can use this code to handle the completion of all uploads:
Promise.all(uploads)
.then(function(results) { /* do the redirect */ })
.catch(function(error) { console.log(error); });
See also: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
Note: Why is counting suggested in an other answer wrong? Because it does not handle the condition when any of the requests fails. You will not be able to detect that case, and you will never reach the condition to be satisfied. Promises (and async/await) are the right way of handling asynchronous operations, like ajax requests.
[Update]
Here is the JQuery approach using the latest JS language features:
var upload = (photo) => {
photo = resizeFile(photo);
let formData = new FormData();
formData.append('photo', photo);
return $.ajax({
type: 'POST',
url: 'upload.php',
data: formData
}) // check other the options of ajax method, you might need them
}
let uploads = files.map(upload);
$.when.apply($,uploads)
.done(_ => { /* do the redirect */ })
.fail(e => { /* handle error */ })
The latter part can be written with async/await syntax:
async somefunction(){
// rest of the code
try {
await $.when.apply($,uploads)
/* do the redirect */
} catch(ex) {
/* handle error */
}
}
Note, that $.ajax returns Deferred, which is a JQuery object, compatible with Promise.

Saving API data to JSON file with NodeJS (or PHP)

Is there a way to save data from an API to a JSON file, with NodeJS using XMLHttpRequest?
The API data is supposed to be displayed on a website, but the API is increcibly slow, so to combat this I would save the data on the server and display the newest data on the website every 5 minutes.
The API is public, the link is http://lonobox.com/api/index.php?id=100002519 if that helps.
Any help is greatly appreciated.
Hey I do a similar thing with a node server that performs basic function on JSON data that I use at work. When it comes to saving the data I just POST it to the server.
But when it come to reading the data I use a XMLHttpRequest to do it, let me illustrate how it works which should give you a good start.
POST file to server.
function processFile(e) {
var file = e.target.result,results;
if (file && file.length) {
$.ajax({
type: "POST",
url: "http://localhost:8080/",
data: {
'data': file
}
}).done(function(msg) {
appendText("Data Saved: " + msg);
});
}
}
From here you can fetch the data with XMLHttpRequest like so...
function getFile(){
var rawFile = new XMLHttpRequest();
rawFile.open("GET", "filename.json", false);
rawFile.onreadystatechange = function ()
{
if(rawFile.readyState === 4)
{
if(rawFile.status === 200 || rawFile.status == 0)
{
var fileText = rawFile.responseText;
}
}
}
rawFile.send(null);
}
Server Code
app.post('/', function(req, res) {
var fileLoc = __dirname.split("\\").length > 1 ? __dirname + "\\public\\filename.json" : __dirname + "/public/filename.json";
fs.writeFile(fileLoc, req.body.data, function(err) {
if (err) {
res.send('Something when wrong: ' + err);
} else {
res.send('Saved!');
}
})
});
Server side requires FS and I use Express for routing.

Renaming a file while opening in browser

I have a requirement as a follows. I user AngularJS, JavaScript.
1. User clicks on a document in the browser. I get the document path and open it. >> window.open(documentpath);
2. But the document which is saved in the directory has a file name replaced as Id and NO extensions. abc/files/4
3. The actual filename is in the DB as Id: 4 Filename: Hello.pdf
So when I open the file, I get abc/files/4 which has no format in it and it doesn't open the file.
How can I open the file with the right name abc/files/Hello.pdf?
1st, I want to take the path abc/files/4 and I don't want to download the file. Just store it somewhere locally like cache/Temp to get the file contents and rename 4 to Hello.pdf and then open it in the browser. All this should happen in the background and should open the file correctly when the user clicks on it.
Is it possible using JavaScript, AngularJS? Please let me know
JavaScript usually has no access to the local file system for security reasons.
What you have to do instead is to pass the file name along with your HTTP response. To do this, add this header to the response:
Content-Disposition: inline; filename="Hello.pdf"
See also:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition
How to set response filename without forcing saveas dialog
How to encode the filename parameter of Content-Disposition header in HTTP?
This will let you download the file with async request and open it in a base64 encoded data url. It WONT set the name, just force the display as pdf. You can use this if you have no access to your server and cant implement Aaron Digulla method which is infinitely better.
/**
*
* jquery.binarytransport.js
*
* #description. jQuery ajax transport for making binary data type requests.
* #version 1.0
* #author Henry Algus <henryalgus#gmail.com>
*
*/
$.ajaxTransport("+binary", function(options, originalOptions, jqXHR) {
// check for conditions and support for blob / arraybuffer response type
if (window.FormData && ((options.dataType && (options.dataType == 'binary')) || (options.data && ((window.ArrayBuffer && options.data instanceof ArrayBuffer) || (window.Blob && options.data instanceof Blob))))) {
return {
// create new XMLHttpRequest
send: function(headers, callback) {
// setup all variables
var xhr = new XMLHttpRequest(),
url = options.url,
type = options.type,
async = options.async || true,
// blob or arraybuffer. Default is blob
dataType = options.responseType || "blob",
data = options.data || null,
username = options.username || null,
password = options.password || null;
xhr.addEventListener('load', function() {
var data = {};
data[options.dataType] = xhr.response;
// make callback and send data
callback(xhr.status, xhr.statusText, data, xhr.getAllResponseHeaders());
});
xhr.open(type, url, async, username, password);
// setup custom headers
for (var i in headers) {
xhr.setRequestHeader(i, headers[i]);
}
xhr.responseType = dataType;
xhr.send(data);
},
abort: function() {}
};
}
});
var blobToBase64 = function(blob, cb) {
var reader = new FileReader();
reader.onload = function() {
var dataUrl = reader.result;
var base64 = dataUrl.split(',')[1];
cb(base64);
};
reader.readAsDataURL(blob);
};
$.ajax("Description.pdf", {
dataType: "binary"
}).done(function(data) {
blobToBase64(data, function(base64encoded) {
window.open("data:application/pdf;base64," + base64encoded);
});
})
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

how to find out if XMLHttpRequest.send() worked

I am using XMLHttpRequest to send a file from javascript code to a django view.I need to detect,whether the file has been sent or if some error occurred.I used jquery to write the following javascript.
Ideally I would like to show the user an error message that the file was not uploaded.Is there some way to do this in javascript?
I tried to do this by returning a success/failure message from django view , putting the success/failed message as json and sending back the serialized json from the django view.For this,I made the xhr.open() non-asynchronous. I tried to print the xmlhttpRequest object's responseText .The console.log(xhr.responseText) shows
response= {"message": "success"}
What I am wondering is,whether this is the proper way to do this.In many articles,I found the warning that
Using async=false is not recommended
So,is there any way to find out whether the file has been sent,while keeping xhr.open() asynchronous?
$(document).ready(function(){
$(document).on('change', '#fselect', function(e){
e.preventDefault();
sendFile();
});
});
function sendFile(){
var form = $('#fileform').get(0);
var formData = new FormData(form);
var file = $('#fselect').get(0).files[0];
var xhr = new XMLHttpRequest();
formData.append('myfile', file);
xhr.open('POST', 'uploadfile/', false);
xhr.send(formData);
console.log('response=',xhr.responseText);
}
My django view extracts file from form data and writes to a destination folder.
def store_uploaded_file(request):
message='failed'
to_return = {}
if (request.method == 'POST'):
if request.FILES.has_key('myfile'):
file = request.FILES['myfile']
with open('/uploadpath/%s' % file.name, 'wb+') as dest:
for chunk in file.chunks():
dest.write(chunk)
message="success"
to_return['message']= message
serialized = simplejson.dumps(to_return)
if store_message == "success":
return HttpResponse(serialized, mimetype="application/json")
else:
return HttpResponseServerError(serialized, mimetype="application/json")
EDIT:
I got this working with the help of #FabrícioMatté
xhr.onreadystatechange=function(){
if (xhr.readyState==4 && xhr.status==200){
console.log('xhr.readyState=',xhr.readyState);
console.log('xhr.status=',xhr.status);
console.log('response=',xhr.responseText);
var data = $.parseJSON(xhr.responseText);
var uploadResult = data['message']
console.log('uploadResult=',uploadResult);
if (uploadResult=='failure'){
console.log('failed to upload file');
displayError('failed to upload');
}else if (uploadResult=='success'){
console.log('successfully uploaded file');
}
}
}
Something like the following code should do the job:
xmlhttp.onreadystatechange = function() {
if (xmlhttp.readyState === 4) {
var response = JSON.parse(xmlhttp.responseText);
if (xmlhttp.status === 200) {
console.log('successful');
} else {
console.log('failed');
}
}
}
XMLHttpRequest objects contain the status and readyState properties, which you can test in the xhr.onreadystatechange event to check if your request was successful.
XMLHttpRequest provides the ability to listen to various events that can occur while the request is being processed. This includes periodic progress notifications, error notifications, and so forth.
So:
function sendFile() {
var form = $('#fileform').get(0);
var formData = new FormData(form);
var file = $('#fselect').get(0).files[0]
var xhr = new XMLHttpRequest();
formData.append('myfile', file);
xhr.open('POST', 'uploadfile/', false);
xhr.addEventListener("load", transferComplete);
xhr.addEventListener("error", transferFailed);
}
function transferComplete(evt) {
console.log("The transfer is complete.");
// Do something
}
function transferFailed(evt) {
console.log("An error occurred while transferring the file.");
// Do something
}
You can read more about Using XMLHttpRequest.

Categories

Resources