Uploading multiple files at once - for Cordova

This post is more than 2 years old.

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.

Initial

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

Images

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:

Raymond Camden's Picture

About Raymond Camden

Raymond is a senior developer evangelist for Adobe. He focuses on document services, JavaScript, and enterprise cat demos. If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can even buy me a coffee!

Lafayette, LA https://www.raymondcamden.com

Archived Comments

Comment 1 by Akash Pal posted on 5/7/2016 at 9:16 PM

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"
}

Comment 2 (In reply to #1) by Akash Pal posted on 5/8/2016 at 12:44 AM

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.

Comment 3 (In reply to #2) by Raymond Camden posted on 5/8/2016 at 1:01 AM

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

Comment 4 (In reply to #3) by Akash Pal posted on 5/8/2016 at 9:31 AM

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?

Comment 5 (In reply to #4) by Raymond Camden posted on 5/10/2016 at 6:00 PM

Can you see if you can recreate this in a simpler demo? Try this one: https://github.com/cfjedima...

Comment 6 (In reply to #5) by Akash Pal posted on 5/11/2016 at 2:35 PM

I actually found a way around by using https://github.com/wymsee/c... .

Comment 7 by Tony posted on 5/24/2016 at 8:19 AM

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!

Comment 8 (In reply to #7) by Raymond Camden posted on 5/24/2016 at 11:30 AM

It shouldn't be difficult at all. Just give it a try.

Comment 9 (In reply to #8) by Tony posted on 5/25/2016 at 4:57 AM

I'll try, but first, how to "transform" this jQuery to angular?

Comment 10 (In reply to #9) by Raymond Camden posted on 5/25/2016 at 12:43 PM

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.

Comment 11 (In reply to #2) by laersheng posted on 5/26/2016 at 4:23 AM

angular or jquery? I replaced jquery with angular, but I got the FormData empty

Comment 12 by Divan van Biljon posted on 12/13/2016 at 11:57 PM

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

Comment 13 (In reply to #12) by Raymond Camden posted on 12/14/2016 at 12:23 AM

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.

Comment 14 (In reply to #13) by Raymond Camden posted on 12/14/2016 at 12:24 AM

Try reader.result.

Comment 15 (In reply to #13) by Divan van Biljon posted on 12/14/2016 at 12:25 AM

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

Comment 16 (In reply to #14) by Divan van Biljon posted on 12/14/2016 at 12:26 AM

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

Comment 17 (In reply to #16) by Raymond Camden posted on 12/14/2016 at 12:29 AM

I really think it will be in the reader object.

Comment 18 (In reply to #17) by Divan van Biljon posted on 12/14/2016 at 12:31 AM

Okay will have a look now and let you know what I find. Hopefully we can sort this out for other people to use :)

Comment 19 (In reply to #17) by Divan van Biljon posted on 12/14/2016 at 12:37 AM

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.

Comment 20 (In reply to #17) by Divan van Biljon posted on 12/14/2016 at 12:53 AM

Is the reader.result/this.result suppose to return an empty object?

Comment 21 (In reply to #17) by Divan van Biljon posted on 12/14/2016 at 12:57 AM

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 {}

Comment 22 (In reply to #17) by Divan van Biljon posted on 12/14/2016 at 1:18 AM

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

Comment 23 by Divan van Biljon posted on 12/14/2016 at 2:30 AM

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

Comment 24 by Divan van Biljon posted on 12/14/2016 at 3:40 AM

Okay so the reader is reading the file in the array, its creating this blob, is this correct?
Blob {size: 72295, type: "image/jpeg"}...

Comment 25 (In reply to #24) by Raymond Camden posted on 12/14/2016 at 3:43 AM

I believe so.

Comment 26 (In reply to #25) by Divan van Biljon posted on 12/14/2016 at 3:45 AM

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?

Comment 27 (In reply to #25) by Divan van Biljon posted on 12/14/2016 at 4:07 AM

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

Comment 28 (In reply to #26) by Raymond Camden posted on 12/14/2016 at 3:33 PM

Sorry - I'm out for the day. I'll try to look more later tonight.

Comment 29 (In reply to #28) by Divan van Biljon posted on 12/14/2016 at 3:45 PM

Cool thanks man I appreciate it, once this is done then my project will be completed :)

Comment 30 by Andrew Watson posted on 8/25/2017 at 3:37 PM

Is it possible to show an icon which will be replaced by photo selected?

Comment 31 (In reply to #30) by Raymond Camden posted on 8/25/2017 at 5:51 PM

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.

Comment 32 by Andrew Watson posted on 8/25/2017 at 7:29 PM

how should i accept the files on server side on Php?

Comment 33 (In reply to #31) by Andrew Watson posted on 8/25/2017 at 7:30 PM

thanks for quick answer, any ideas how to accept the files on php?

Comment 34 (In reply to #32) by Raymond Camden posted on 8/25/2017 at 7:31 PM

I don't know - I use Node, and in the past, ColdFusion.

Comment 35 (In reply to #33) by Raymond Camden posted on 8/25/2017 at 7:31 PM

See my other comment.

Comment 36 (In reply to #34) by Andrew Watson posted on 8/25/2017 at 7:33 PM

thanks again for quick answer :)
i am just wondering what is the variable name upon receiving

Comment 37 (In reply to #36) by Raymond Camden posted on 8/25/2017 at 7:34 PM

Ah, see this line: fd.append('fileName'+(images.indexOf(i)+1), file.name);

basically fileName1, fileName2, etc.

Comment 38 (In reply to #37) by Andrew Watson posted on 8/25/2017 at 7:37 PM

Thanks a lot for your solution, it is saved a lot of time for me :)

Comment 39 (In reply to #38) by Raymond Camden posted on 8/25/2017 at 7:37 PM

Cool - if you can bill a rich client, visit the Amazon wish list. ;)

Comment 40 (In reply to #39) by Andrew Watson posted on 8/25/2017 at 7:43 PM

You are the best, Raymond :)

Comment 41 by Andrew Watson posted on 8/25/2017 at 8:51 PM

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.

Comment 42 (In reply to #41) by Raymond Camden posted on 8/25/2017 at 8:54 PM

Yes - the Camera plugin supports doing either or.

Comment 43 (In reply to #42) by Andrew Watson posted on 8/25/2017 at 9:44 PM

alright, thank you :)

Comment 44 by Andrew Watson posted on 8/26/2017 at 1:35 PM

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';
}
}

Comment 45 (In reply to #44) by Raymond Camden posted on 8/26/2017 at 1:40 PM

Use dev tools to debug the app and check the network panel. Confirm it is only sending one file.

Comment 46 (In reply to #45) by Andrew Watson posted on 8/26/2017 at 2:27 PM

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?

Comment 47 (In reply to #46) by Raymond Camden posted on 8/26/2017 at 2:39 PM

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.

Comment 48 (In reply to #47) by Andrew Watson posted on 8/26/2017 at 8:38 PM

alright, thanks a lot for support

Comment 49 by Nur Khasanah posted on 4/3/2019 at 6:09 PM

please update this source code into Ionic 3 version

Comment 50 (In reply to #49) by Raymond Camden posted on 4/3/2019 at 6:20 PM

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.

Comment 51 (In reply to #50) by Nur Khasanah posted on 4/3/2019 at 7:05 PM

okay thanks. is it possible if I insert multiple images into multiple file keys and uploading to server ?

Comment 52 (In reply to #51) by Raymond Camden posted on 4/3/2019 at 7:09 PM

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.

Comment 53 (In reply to #52) by Nur Khasanah posted on 4/3/2019 at 7:17 PM

thanks. no problem anymore.