Chaining multiple Cordova File Transfers with ngCordova

This post is more than 2 years old.

One issue you may run into with the FileTransfer plugin is that it only lets you do one transfer at a time. You can get around this by using XHR2 (for uploads anyway), but I thought it would be nice to demonstrate how to work with multiple transfers using promises. The FileTransfer plugin does not use promises by default, but luckily you can simply use ngCordova and use the promisified (that's a word) version of the plugin.

For this demo I'm using Ionic and ngCordova, although in theory you can use ngCordova without Ionic. I created a new application with the blank template. I then used bowser to install ngCordova, which wasn't actually necessary since the CLI supports adding it:

I then whipped up the following demo. I don't like putting everything in one JS file for my Angular projects, but since this is just a demo, I guess that's ok.

angular.module('starter', ['ionic','ngCordova'])
.config(['$compileProvider', function($compileProvider) {

	   $compileProvider.imgSrcSanitizationWhitelist(/^\s*(https?|ftp|mailto|content|file|assets-library):/);

}])
.controller('Main', ["$ionicPlatform", "$cordovaFileTransfer", "$q", "$scope",
    function($ionicPlatform, $cordovaFileTransfer, $q, $scope) {
    console.log("running Main controller");
    $scope.images = [];

    $ionicPlatform.ready(function() {
      //resources to download
      var resources = [
        "https://placekitten.com/g/200/300",
        "https://placekitten.com/g/200/350",
        "https://placekitten.com/g/400/350",
        "https://placekitten.com/g/300/200",
        "https://placekitten.com/g/188/188"
      ];

      var promises = [];

      resources.forEach(function(i,x) {
        var targetPath = cordova.file.documentsDirectory + "image"+x+".jpg";
        promises.push($cordovaFileTransfer.download(i, targetPath, {}, true));
      });

      $q.all(promises).then(function(res) {
        console.log("in theory, all done");
        for(var i=0; i<res.length; i++) {
          $scope.images.push(res[i].nativeURL);
        }
      });
    });

}])
.run(function($ionicPlatform) {
  $ionicPlatform.ready(function() {
    // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
    // for form inputs)
    if(window.cordova && window.cordova.plugins.Keyboard) {
      cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
    }
    if(window.StatusBar) {
      StatusBar.styleDefault();
    }
  });
});

Ok, let's take this from the top. First off - the config block is just there to allow for File based URIs in my dom. Angular is a bit anal (ok, secure) about what it allows for image urls. The next bit is my controller, Main. I've got 5 URLs for different kittens at placekitten.com. I create an array to store my promises, and then just loop over the URLs. For each one I call the ngCordova-wrapped file transfer download method. Since it returns a promise I end up with an array of promises.

Finally, I use Angular's all method for their $q library to simply say, "Do this when they are all done." I then push the final URLs into an array that is used in my view. Here's the index.html:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">
    <title></title>

    <link href="lib/ionic/css/ionic.css" rel="stylesheet">
    <link href="css/style.css" rel="stylesheet">

    <!-- IF using Sass (run gulp sass first), then uncomment below and remove the CSS includes above
    <link href="css/ionic.app.css" rel="stylesheet">
    -->

    <!-- ionic/angularjs js -->
    <script src="lib/ionic/js/ionic.bundle.js"></script>

    <script src="lib/ngCordova/dist/ng-cordova.js"></script>
    <script src="cordova.js"></script>

    <!-- your app's js -->
    <script src="js/app.js"></script>
  </head>
  <body ng-app="starter">

    <ion-pane>
      <ion-header-bar class="bar-stable">
        <h1 class="title">Ionic Blank Starter</h1>
      </ion-header-bar>
      <ion-content ng-controller="Main">
        <div ng-repeat="img in images">
          <img ng-src="{{img}}">
        </div>
      </ion-content>
    </ion-pane>
  </body>
</html>

And that's it. The result is a set of cat pictures, which, to be honest, is what all apps should end up with:

iOS Simulator Screen Shot Apr 13, 2015, 11.24.03 AM

Similar code would work for uploads as well. Again, you do not need to use Ionic/ngCordova for this. You could create your own promises and do this by hand with a bit more work. (I've got a vide on deferreds and jQuery that may make this easier for you.)

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 JerryBels posted on 4/13/2015 at 7:44 PM

That's exactly what I came up with also. Nice tutorial ! Thanks :)

Comment 2 by Gaurav Chandra posted on 5/9/2015 at 8:52 AM

how can I move the code for downloading from controller to a service?

Comment 3 (In reply to #2) by Raymond Camden posted on 5/9/2015 at 12:59 PM

Just do it? I mean - just put it in a service and call the service method.

Comment 4 (In reply to #3) by Gaurav Chandra posted on 5/9/2015 at 2:48 PM

Sorry for the stupid question. I was able to create a service after 5 min of having a mug of coffee :)

Comment 5 (In reply to #4) by Raymond Camden posted on 5/9/2015 at 3:13 PM

Nah, not stupid. As it stands, when I was learning Angular (wait, I still am), I did a lot in my controllers and rarely used services. This post should use a service!

Comment 6 by Julian posted on 5/17/2015 at 11:12 AM

Again, great post.
One question though, I am working on app where on the first start I have to download 400 image assets. It looks like $q.all is trying to run all at the same time which is practically crashing the whole Application.

Any thoughts how to overcome this kind of problem?

Comment 7 (In reply to #6) by Raymond Camden posted on 5/17/2015 at 1:22 PM

You can download a zip and unzip it client-side, but a 400 image zip might be rather large still. You could do 100 at a time, which could help.

Comment 8 (In reply to #7) by Julian posted on 5/17/2015 at 2:18 PM

Didn't think about putting it into a zip file. I will need to talk to the server-side developers to change it but the effort would be worth it. Thanks

Comment 9 (In reply to #8) by Raymond Camden posted on 5/17/2015 at 2:34 PM

I'm being lazy right now, but if you search my blog, I wrote up this process a few years back.

Comment 10 (In reply to #8) by Raymond Camden posted on 5/17/2015 at 2:36 PM
Comment 11 by HaroonDilshad posted on 11/9/2015 at 2:22 AM

Hey Raymond, great posting. For the promises, the success callbacks are firing just fine but not the progress update. I was skeptical ngCordova wasn't firing them but then I did only one upload without promises and the progress update fired fine.

$q.all(promises).then(function(result) {
// Success!
}, function(err) {
// Error
}, function (progress) {
// constant progress updates
});
Any idea where I'm going wrong?

Comment 12 (In reply to #11) by Raymond Camden posted on 11/9/2015 at 2:29 PM

I think you are misunderstanding $q.all(....).then. This code is saying, "When all of the promises have successfully completed, do X". In that scenario, we aren't listening for the progress events at all. You would need to add your own listeners to *every* request and then somehow manage them in such a way that it made sense visually. Keep in mind each is getting a %, but you don't know if file 2 is bigger than file 1. It's going to be near impossible to manage imo.

Comment 13 by karim posted on 12/2/2015 at 8:47 AM

Thank you, nice tutorial.

Comment 14 by sunset posted on 1/28/2016 at 5:53 PM

Hi! I'm a starter in Ionic. This is very helpful arc for me.
But I have a question.
In the above sample codes, how can I show the progress for downloading?
Hope your help.

Comment 15 (In reply to #14) by Raymond Camden posted on 1/28/2016 at 6:19 PM

In theory you can add progress event handlers to each process, but representing the status in a way that makes sense would be difficult. If you have 3 transfers and 1 is 23% done and 2 is 91% done and 3 is 5% done, what's the status? It isn't impossible, just clunky.

I think you can bind a complete handler for each one and keep a counter so you could at least say "X of Y done."

Comment 16 (In reply to #15) by sunset posted on 1/28/2016 at 10:44 PM

Thanks for your reply.
I hope the details.
so I think total progress. If I have 3 transfers and 1 is 23 % done and 2 is 91% done and 3 is 5% done, and then the total progress is (23 + 91 + 5) / 3 = 39 %.
Can I calculate this progress during downloading?
How about your think?

Comment 17 (In reply to #16) by Raymond Camden posted on 1/29/2016 at 10:35 PM

I'm not sure if that math is right, but sure, go for it. :) To monitor progress, see the FileTransfer docs. It shows how to specify a call back for it.

Comment 18 (In reply to #17) by sunset posted on 1/30/2016 at 1:26 AM

Thank you. Thank you.
And I have a problem. I used your code to download image files in main controller.
After in another controller, I'm trying to use the image.
But I can get the native file path, but it isn't show image tag.
How can I do? Help me!
My codes are the following.

another controller

var fname = "001.jpg";

$cordovaFile.checkFile(cordova.file.externalCacheDirectory, fname)

.then(function(obj) {

$scope.img_url = obj.nativeURL;
$scope.$apply();

}, function(error) {
alert(JSON.stringify(error));
});

I hope your help.

Best regards.

Comment 19 (In reply to #18) by Raymond Camden posted on 1/30/2016 at 1:34 PM

Do you see any errors in your console when you test w/ Chrome Remote Debug or Safari Remote Debug?

Comment 20 (In reply to #19) by sunset posted on 1/30/2016 at 4:35 PM

When I test Chrome broswer, I can't the downloads the file.
error;
XMLHttpRequest cannot load http://projects.techindusta.... No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:8100' is therefore not allowed access.

So I tested it on device. But I can't see the error alert.
What's the matter?

Comment 21 (In reply to #20) by Raymond Camden posted on 1/30/2016 at 8:01 PM

Did you try remote debugging?

Comment 22 by Alex Lyalyuk posted on 3/7/2016 at 1:50 PM

$q.all(promises) runs all promises downloading at the same time. I have more than 500 images how to make downloading in threads? Because 500+ queries to the webserver is too much and I have a lot of connection errors. Also I thing it should have some timeout between connection. Do you have any ideas how to solve my problem?

Comment 23 (In reply to #22) by Raymond Camden posted on 3/7/2016 at 4:02 PM

Well, there's a couple of things to consider.

First, check out the ContentSync plugin. It lets you point to a zip and the plugin handles grabbing it and extracting it. It could be perfect for this.

Another idea would be to consider 'batching' up the 500 downloads into bundles of 10-25 or so. Let the user grab some, and then grab another bunch after the first is done.

Comment 24 (In reply to #23) by Alex Lyalyuk posted on 3/9/2016 at 3:41 PM

Thank you for your answer!
I separated downloading process into bundles. I wrapped $cordovaFileTransfer.download in anonymous function. And I call each download recursively.
Now i have another trouble - It works perfectly with small images (less than 30kb); But it doesn`t work with large files. How to fix it?

Comment 25 (In reply to #24) by Alex Lyalyuk posted on 3/9/2016 at 6:34 PM

I`ve solved this problem by removing options, trustHosts properties

Comment 26 by Patrick Lau posted on 4/4/2016 at 7:24 AM

Hi, I am new to ionic framework and I tried to use your method to upload multiple images. I am now stuck with the problem that the upload result seems to be inconsistent, not all my images were uploaded, only a partial of the images were uploaded and the number varies each time I try the upload. What could be the problem?

Comment 27 (In reply to #26) by Raymond Camden posted on 4/4/2016 at 1:02 PM

Do you see anything interesting in your devtools console?

Comment 28 by Owen Williams posted on 5/26/2016 at 6:05 AM

Cheers Ray! Great tips, just applied it to my project, cheers!

Comment 29 by Manoj Kumar Mobile App Dept posted on 9/21/2017 at 6:53 AM

great but if there is file transfer error it's shows me multiple time how i can stop for-each on first file transfer error

Comment 30 (In reply to #29) by Raymond Camden posted on 9/21/2017 at 12:31 PM

So yeah - my code assumes they all work and doesn't have a catch clause for the Promise.all. If you add that, you should be able to check the results of the errors there.

Note though I would not use ngCordova w/ Ionic, but rather Ionic Native with Ionic 2+.

Comment 31 by Manoj Kumar Mobile App Dept posted on 9/22/2017 at 5:03 AM

DOWNLOAD 20 FILES AT A TIME WITH IONIC 3 WITH ERROR HANDLING

https://jsfiddle.net/uve5e745/

Comment 32 (In reply to #31) by Raymond Camden posted on 9/22/2017 at 11:07 AM

Please do not post large blocks of code as comments. Next time link to a codepen or Gist.

Comment 33 (In reply to #32) by Manoj Kumar Mobile App Dept posted on 9/22/2017 at 12:36 PM

SURE SIR....