Yesterday I blogged about how you could use XHR2 to upload multiple files in one POST operation. This was a followup to an earlier post (Processing multiple simultaneous uploads with Cordova) discussing how to send multiple files with the Cordova FileTransfer plugin.
Unfortunately, while my post yesterday will work fine on the desktop, it won't work in Cordova apps that disable the use of file inputs. Another issue is that it doesn't demonstrate how you would integrate it with the Camera. I've figured out how to get this working so let's take a look.
First off - be sure to look at the older older demo so you have a basic idea of the functionality. Basically we've got an app with 2 buttons - select a picture and upload.

When you select a picture, it is 'drawn' onto the app so you can see what you will be uploading.

All of this code can be found on the earlier blog entry so I won't go over it again. Instead, let's focus on the upload part.
We've got an array of image URIs that represent paths on the device file system. My initial plan was to loop over the array and create File objects from each URI. My first draft was something like this:
function uploadPics() {
console.log("Ok, going to upload "+images.length+" images.");
var defs = [];
var fd = new FormData();
images.forEach(function(i) {
console.log('processing '+i);
var def = $.Deferred();
window.resolveLocalFileSystemURL(i, function(fileEntry) {
console.log('got a file entry');
fileEntry.file(function(file) {
console.log('now i have a file ob');
console.dir(file);
fd.append('file', file);
def.resolve();
}, function(e) {
console.log('error getting file', e);
});
}, function(e) {
console.log('Error resolving fs url', e);
});
defs.push(def.promise());
});
$.when.apply($, defs).then(function() {
console.log("all things done");
var request = new XMLHttpRequest();
request.open('POST', 'http://192.168.5.13:3000/upload');
request.send(fd);
});
}
Basically we turn the URL into a FileEntry object and then turn that into a File object because the FileSystem has to be complex so we can all keep our jobs. The crucial part is this: fd.append('file',file)
. This should have worked exactly like the code from yesterday's post. However, something weird happened. When the post was sent, my Node server never got any file data. When I used dev tools to inspect the network data, I saw this:
------WebKitFormBoundaryCvgXBJta2kHYiUd9
Content-Disposition: form-data; name="file"
[object Object]
------WebKitFormBoundaryCvgXBJta2kHYiUd9--
For some reason toString
was being called on the File object when passed to Form data. I then tried to pass a blob:
fd.append('file', file.slice(0,file.size));
But that failed too. I did some searching and came across the solution (I'll credit them at the end!) - using a blob was right, but I really needed to use a FileReader to get the data. Now - I thought my file.slice
code was doing the same, but... I don't know. It just didn't work. Adding in a FileReader finally did it. Here is the final part of the code (with another modification I'll explain in a bit as well):
function uploadPics() {
console.log("Ok, going to upload "+images.length+" images.");
var defs = [];
var fd = new FormData();
images.forEach(function(i) {
console.log('processing '+i);
var def = $.Deferred();
window.resolveLocalFileSystemURL(i, function(fileEntry) {
console.log('got a file entry');
fileEntry.file(function(file) {
console.log('now i have a file ob');
console.dir(file);
var reader = new FileReader();
reader.onloadend = function(e) {
var imgBlob = new Blob([this.result], { type:file.type});
fd.append('file'+(images.indexOf(i)+1), imgBlob);
fd.append('fileName'+(images.indexOf(i)+1), file.name);
def.resolve();
};
reader.readAsArrayBuffer(file);
}, function(e) {
console.log('error getting file', e);
});
}, function(e) {
console.log('Error resolving fs url', e);
});
defs.push(def.promise());
});
$.when.apply($, defs).then(function() {
console.log("all things done");
var request = new XMLHttpRequest();
request.open('POST', 'http://192.168.5.13:3000/upload');
request.send(fd);
});
}
Ok, so you can see the FileReader in play so I've got one more level of callback hell, but this isn't too bad (more like Callback Heck).
Edit on May 9th: Please see Akash's comment where he noted the third argument to FormData.append allows you to specify a file name. I missed that! It should be used instead of my workaround here.
One thing I discovered though was that my Node server wasn't getting a good file name - it showed up as 'blob'. I added a new form field called fileNameX (X being an index) that included the file name. So on the server I have fileX which is the actual file and fileNameX which is the file name. Unfortunately, Android passes 'content' as the name. I decided to stop messing with the code at that point. You could add a bit of logic in there to rename it client-side or even do it server-side (perhaps a simple UUID). But I can confirm it works in both Android and iOS.
You can find the full source code here: https://github.com/cfjedimaster/Cordova-Examples/tree/master/multiupload2
Credit for this workaround goes to two people with similar solutions:
- This gist by Fesor.
- This StackOverflow solution by Joe Komputer.
Archived Comments
I actually got most of the part, even am able to upload multiple images and as you have mentioned even the server at my end ; ended up getting a blob object. However i do not have the control over server. I replaced these two lines
fd.append('file'+(images.indexOf(i)+1), imgBlob);
fd.append('fileName'+(images.indexOf(i)+1), file.name);
with
fd.append('attachments', imgBlob);
Now from this position how do I give this image blob a fileName and mime type
without adding a new form field.
My response currently looks like this:
"attachments": {
"multimediaId": 538073,
"multimediaType": "unknown",
"text": "",
"url": "/c/document_library/get_file?groupId=509206&folderId=538060&title=-806640443blob",
"usedFor": "ATTACHMENT"
}
I actually solved the issue, I replaced following lines
fd.append('file'+(images.indexOf(i)+1), imgBlob);
fd.append('fileName'+(images.indexOf(i)+1), file.name);
with
fd.append('attachments',imgBlob,'sky.jpg');
I just provided a third parameter with a file-name.
So the server no longer received blob objects.
Thank you for the assistance. So would be procedure remain same if instead of images some kind document is used.
Yep - you are perfectly correct and that is documented in the append method. I wish I had seen that - thank you for sharing. Monday I'll edit the post to point out your comment below. (Since some folks never read the comments.)
Okay that's perfectly all right. Well I came across another issue in android ; when I add a picture from the recent menu (1st image) the image is not rendered and it gets broken.(2nd image). However upon uploading its gets rendered properly.(image 3). The the first is broke under the Download section is the one which wasn't rendered.
However this issue doesn't occur if I select select an image from the gallery or download section.
What maybe the probable reason for this cause?
Can you see if you can recreate this in a simpler demo? Try this one: https://github.com/cfjedima...
I actually found a way around by using https://github.com/wymsee/c... .
Hi Raymond, how hard is to use this in Ionic app? I'm using http://ngcordova.com/docs/p... , how to modify your example using this plugin?
Thank you!
It shouldn't be difficult at all. Just give it a try.
I'll try, but first, how to "transform" this jQuery to angular?
Well, I'm not sure there is *one* way of doing it - but I'd create a service to handle taking an array of file obs and sending it to the server where you want stuff stored. Or you could just do it in the controller. That's sounds "impure" in terms of building proper Angular apps, but it may 'just work' for you.
angular or jquery? I replaced jquery with angular, but I got the FormData empty
Hi, I've tried implementing this in Ionic 2 but I'm getting the following errors
Error:
Error in Success callbackId: File1313867162 : [object Object]
Uncaught [object Object]callbackFromNative @ cordova.js:314(anonymous function) @ VM107:1
cordova.js:312 Error in Success callbackId: File1313867163 : TypeError: Cannot read property 'result' of undefined
Code:
http://pastebin.com/ix2Fzfv0
I'd say look at the arguments passed to the success handler and see why "result" wasn't there.
Ah - I see you are using ES - "this" is being bound different I bet. Just... figure out where result should be. :) Start there.
Try reader.result.
How would you propose I go about doing that? I'm still new to angular 2 but if I have sort of an Idea where to look then I'll know xD
Okay I'll give it a try, I take it i should check out the file parameter being passed through correct? from this
entry.file(function(file) {
I really think it will be in the reader object.
Okay will have a look now and let you know what I find. Hopefully we can sort this out for other people to use :)
Okay so I've replaced it with reader.result so I believe you are correct that it was in the object. I now get a different error regarding the file name I'm passing through as It cant find the property 'venue_images' which is passed through to the fd.append.
Is the reader.result/this.result suppose to return an empty object?
FileReader {_readyState: 2, _error: null, _result: ArrayBuffer, _progress: 9843, _localURL: "cdvfile://localhost/cache-external/IMG_20161214_025141.jpg"…}
This is what is returned when I console.log(reader)
And console.log(reader.result) returns ArrayBuffer {}
Okay I've got it to not throw any errors anymore but now for some reason it's not appending anything to the 'fd'. When I console.log(fd), it returns an empty object
Hey man. Okay so I've managed to post the data to server except for the image files, for some reason that is not being sent to the server, basically its not being appended to the FormData which I can't understand why :(
Here is a Pastebin for the code im currently using: http://pastebin.com/aUfBwMdq
Okay so the reader is reading the file in the array, its creating this blob, is this correct?
Blob {size: 72295, type: "image/jpeg"}...
I believe so.
the thing is it's still not appending and im not seeing it server side or the network call being made with the passed parameters?
Provisional headers are shown
Accept:application/json, text/plain, */*
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryAW2uo8dUCJ2x6u13
Origin:file://
User-Agent:Mozilla/5.0 (Linux; Android 6.0.1; SM-G900H Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Crosswalk/22.52.561.4 Mobile Safari/537.36
That is the Request Headers output.
Request payload
------WebKitFormBoundaryAW2uo8dUCJ2x6u13
Content-Disposition: form-data; name="id"
58504f5ace291b1d4036a7ba
------WebKitFormBoundaryAW2uo8dUCJ2x6u13
Content-Disposition: form-data; name="venue"
{"name":"","slogan":"","address":"","description":""}
------WebKitFormBoundaryAW2uo8dUCJ2x6u13--
Sorry - I'm out for the day. I'll try to look more later tonight.
Cool thanks man I appreciate it, once this is done then my project will be completed :)
Is it possible to show an icon which will be replaced by photo selected?
Sure - you would add N images to the page with an icon, and as you upload each one, change the src on an image to the new source.
how should i accept the files on server side on Php?
thanks for quick answer, any ideas how to accept the files on php?
I don't know - I use Node, and in the past, ColdFusion.
See my other comment.
thanks again for quick answer :)
i am just wondering what is the variable name upon receiving
Ah, see this line: fd.append('fileName'+(images.indexOf(i)+1), file.name);
basically fileName1, fileName2, etc.
Thanks a lot for your solution, it is saved a lot of time for me :)
Cool - if you can bill a rich client, visit the Amazon wish list. ;)
You are the best, Raymond :)
One more question, when i tap to select a photo, it opens the gallery of images. Is it possible to take a photo directly from camera? E.g. to show a message box first: Upload existing image or Take a picture.
Thank you.
Yes - the Camera plugin supports doing either or.
alright, thank you :)
It sends only 1 file, i am not understanding whether the issue is on cordova or on server side. The file sent is the latest selected file, all others are being ignored. Any advice would be appreciated.
Accepting the files with following code:
for($i=1; $i<10;$i++)
{
$filename = $_FILES["file".$i]["name"];
$newfilename = md5($filename) .date(isu).".png";
if(@copy($_FILES['file'.$i]['tmp_name'],"uploads/". $newfilename))
{
$messages[] = $_FILES['file'.$i]['name'].' uploaded';
}
else
{
/*** an error message ***/
$messages[] = 'Uploading '.$_FILES['file'.$i]['name'].' Failed';
}
}
Use dev tools to debug the app and check the network panel. Confirm it is only sending one file.
alright, the problem is on server side.
If i change the server side code to accept file number 2 it accepts it. But if create variables to accept file 1, file 2 etc. it accepts only file 1. Any suggestions?
Well, I don't know PHP so I don't know what to tell you. Generally, your code needs to say: is there form field called fileX? if so, grab it. then check for fileX+1. And keep doing that till fileX+1 doesn't exist.
alright, thanks a lot for support
please update this source code into Ionic 3 version
The code I shared here would work fine in Ionic 1, 2, or 3. The focus was on the JS code to upload data, not the UI/UX front end, so it should "just work" in Ionic. Not as a copy and paste of course, but you get the idea.
okay thanks. is it possible if I insert multiple images into multiple file keys and uploading to server ?
I'm not quite sure what you are asking. I'm associating each image with a name, fileX, where X increments. So on the server it would be file1, file2, etc.
thanks. no problem anymore.