Syncing Data With Dropbox Using Ionic Framework

by - May 31, 2016

We have seen that in the apps being used now-a-days, it is required to store or sync data remotely.

The current generation is all about the cloud and finding ways to be a part of it. Ionic Framework is a great way to make hybrid cross platform mobile Android and iOS applications, but how about sharing data between them?
Dropbox is among the few cloud storage services that have have great APIs and documentation.

Below here, I mention how I synced data in Ionic Framework using Dropbox’s Datastore API. The following is not the best way but a successful one.




These instructions will make use of Dropbox’s JavaScript SDK and Apache Cordova’s InAppBrowser plugin.
Because we are in an app environment rather than a web browser,
we cannot make use of only the JavaScript SDK.

Let’s start by creating a new Ionic project and adding the Android and iOS platforms:

Create Ionic Project For Android And iOSShell
    ionic start ExampleProject blank
    cd ExampleProject
    ionic platform add android
    ionic platform add ios

You must note that if you are not on a Mac computer, you cannot add and build for the iOS platform.

This tutorial is going to work for both - Mac and iOS as well.

Next is we need to install the InAppBrowser plugin:

Install Apache Cordova InAppBrowser PluginShell

    cordova plugin add https://git-wip-us.apache.org/repos/asf/cordova-plugin-inappbrowser.git

Hereby, it is mandatory that we become familiar with what our intentions are with this Dropbox API.
Since, we will ot be working with files, hence we will not be making use of Core Sync API.
We plan to work only and only with data and the Datastore API. This will give us an object-based
cloud storage option.

Surely, there are restrictions in terms of storage space, but for saving app preferences or small pieces of data, there shouldn’t be any issues.

For usage of the API, registration of the application with Dropbox is a must. Hence, we choose Datastore API during this process.
 
Once created, make sure to list http://localhost/callback as your callback / redirect URI.
This is because we are using an app and not a website.
Honestly, it doesn’t really matter what you list it as, but for this tutorial use what I said.

Finally, we need the the application key from the app page. Make note of this because we are going to be using it with the JavaScript SDK.

With the Dropbox Datastore JavaScript SDK downloaded, place the JavaScript file in your www/js directory and include it in your index.html file like so:

index.htmlXHTML
   <!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">
            <script src="lib/ionic/js/ionic.bundle.js"></script>
            <script src="cordova.js"></script>
            <script src="js/dropbox-datastores-1.2.0.js"></script>
            <script src="js/app.js"></script>
        </head>
        <body ng-app="starter">

       
       

Even though, the official JavaScript SDK will be used, we still need to implement our own Oauth solution.


The Oauth solution bundled with the SDK isn't going to work because we need to launch in an external browser.
Here, the InAppBrowser plugin comes into play.

app.jsJavaScript
    var browserRef = window.open("https://www.dropbox.com/1/oauth2/authorize?client_id="; + appKey + "&redirect_uri=http://localhost/callback"; + "&response_type=token", "_blank", "location=no");
    browserRef.addEventListener("loadstart", function(event) {
        if((event.url).startsWith("http://localhost/callback")) {
            var callbackResponse = (event.url).split("#")[1];
            var responseParameters = (callbackResponse).split("&");
            var parameterMap = [];
            for(var i = 0; i < responseParameters.length; i++) {
                parameterMap[responseParameters[i].split("=")[0]] = responseParameters[i].split("=")[1];
            }
            if(parameterMap["access_token"] !== undefined && parameterMap["access_token"] !== null) {
                var response = {
                    access_token: parameterMap["access_token"],
                    token_type: parameterMap["token_type"],
                    uid: parameterMap["uid"]
                }
            } else {
                alert("There was a problem authorizing");
            }
            browserRef.close();
        }
    });

The above chunk of code closely resembles my previous article regarding Oauth with Ionic Framework.

Now we use GET /1/oauth2/authorize as our endpoint with a few parameters, as per the Dropbox documentation.

Here, the process gets initialised. Once complete, it will redirect you to your redirect uri.

After the InAppBrowser listener detects the redirect, we then start parsing what we see in the URL.
 
We are looking for the access token, token type, and uidin particular.

We can assume that the login was successful if we find them. Thereon, we can start using the datastore features.

Lucky for us, the official JavaScript Datastore SDK can be used without issue after feeding it an
access token.

app.jsJavaScript
    dropboxClient = new Dropbox.Client({key: dropboxAppKey, token: response.access_token, uid: response.uid});

Per the documentation, the first thing we want to do is get the default datastore and any tables
we wish to use from our datastore manager.

app.jsJavaScript
    var datastoreManager = dropboxClient.getDatastoreManager();
    datastoreManager.openDefaultDatastore(function (error, datastore) {
        if(error) {
            alert('Error opening default datastore: ' + error);
        }
        var taskTable = datastore.getTable('tasks');
    });


Since we have our table in hand, we can start querying or inserting data.

The JavaScript SDK for datastores was never meant to sync with a local copy. Or at least that is what the documentation has lead me to believe.
So we need to check out the correctness of the official documentation.

We need to store our data locally if we want the syncing of it only when possible. This is because we want our mobile applications to function
with and without a network connection.

For this, I’ve accomplished this strategy:

Search for undocumented local records
Push undocumented local records to Dropbox
Pull all records from Dropbox and replace all local data
So in my strategy, I determine all undocumented local records to be records
without a Dropbox record id. Every record in Dropbox has an id, so it is safe to say anything without,
is not yet on Dropbox. Once I push all these records to Dropbox, we can now replace our local data with
the remote data because the remote data is now potentially more up to date than local.

This may not be an efficient way to do business, but it surely is an effective one.
If your aim is to be efficient, try to pull remote records that don’t exist
locally rather than all records.


On ecan also make use of the recordsChanged event listener.
Many different ways to handle synchronization between local and remote.

If you’re interested in following the way I did things for data sync, you can use the
following AngularJS service that I made:

Dropbox AngularJS Apache Cordova ServiceJavaScript
    myDropboxApp.service("DropboxService", function($q, $ionicLoading) {
   
        this.dropboxAppKey = null;
        this.dropboxClient = null;
        this.dropboxAccessToken = null;
        this.dropboxUid = null;
        this.datastoreManager = null;
        this.datastoreTable = [];
   
        /*
        * Store the Dropbox Datastore API app key
        *
        * @param string appKey
        * @return
        */
        this.init = function(appKey) {
            this.dropboxAppKey = appKey;
        }
   
        /*
        * Authenticate with Dropbox Oauth 2.0. If an access token is not already stored in memory, perform
        * the oauth process, otherwise use the token in memory
        *
        * @param
        * @return
        */
        this.authenticate = function() {
            var deferred = $q.defer();
            if(window.localStorage.getItem("dropboxCredentials") !== undefined && window.localStorage.getItem("dropboxCredentials") !== null) {
                deferred.resolve(JSON.parse(window.localStorage.getItem("dropboxCredentials")));
            } else {
                var browserRef = window.open("https://www.dropbox.com/1/oauth2/authorize?client_id="; + this.dropboxAppKey + "&redirect_uri=http://localhost/callback"; + "&response_type=token", "_blank", "location=no,clearsessioncache=yes,clearcache=yes");
                browserRef.addEventListener("loadstart", function(event) {
                    if((event.url).startsWith("http://localhost/callback")) {
                        var callbackResponse = (event.url).split("#")[1];
                        var responseParameters = (callbackResponse).split("&");
                        var parameterMap = [];
                        for(var i = 0; i < responseParameters.length; i++) {
                            parameterMap[responseParameters[i].split("=")[0]] = responseParameters[i].split("=")[1];
                        }
                        if(parameterMap["access_token"] !== undefined && parameterMap["access_token"] !== null) {
                            var promiseResponse = {
                                access_token: parameterMap["access_token"],
                                token_type: parameterMap["token_type"],
                                uid: parameterMap["uid"]
                            }
                            this.dropboxAccessToken = parameterMap["access_token"];
                            this.dropboxUid = parameterMap["uid"];
                            deferred.resolve(promiseResponse);
                        } else {
                            deferred.reject("Problem authenticating");
                        }
                        browserRef.close();
                    }
                });
            }
            return deferred.promise;
        }
   
        /*
        * Connect and cache the specified datastore tables
        *
        * @param Array[string] datastoreTableNames
        * @return
        */
        this.connect = function(datastoreTableNames) {
            var deferred = $q.defer();
            var global = this;
            this.authenticate().then(function(response) {
                window.localStorage.setItem("dropboxCredentials", JSON.stringify(response));
                global.dropboxClient = new Dropbox.Client({key: global.dropboxAppKey, token: response.access_token, uid: response.uid});
                global.setDatastoreManager(global.dropboxClient.getDatastoreManager(), datastoreTableNames).then(function() {
                    deferred.resolve("Connected and obtained datastore tables");
                }, function(error) {
                    deferred.reject(error);
                });
            }, function(error) {
                deferred.reject(error);
            });
            return deferred.promise;
        }
   
        /*
        * Clear everything from the Dropbox service, essentially logging out
        *
        * @param
        * @return
        */
        this.disconnect = function() {
            window.localStorage.removeItem("dropboxCredentials");
            this.dropboxClient = null;
            this.dropboxUid = null;
            this.dropboxAccessToken = null;
            this.datastoreManager = null;
        }
   
        /*
        * Sync all local data to the Dropbox cloud and then replace all local data with cloud data
        *
        * @param string datastoreTableName
        * @param Array[string] tableFields
        * @return
        */
        this.sync = function(datastoreTableName, tableFields) {
            if(this.dropboxClient == null) {
                return;
            }
            this.syncAllUp(datastoreTableName);
            this.syncAllDown(datastoreTableName, tableFields);
        }
   
        /*
        * Download all data from the Dropbox cloud and replace whatever is stored locally
        *
        * @param string datastoreTableName
        * @param Array[string] tableFields
        * @return
        */
        this.syncAllDown = function(datastoreTableName, tableFields) {
            if(this.dropboxClient == null) {
                return;
            }
            var localStorageObject = JSON.parse(window.localStorage.getItem(datastoreTableName));
            var remoteStorageObject = [];
            if(this.datastoreTable[datastoreTableName] != null) {
                var dropboxStorageObject = this.datastoreTable[datastoreTableName].query();
                for(var i = 0; i < dropboxStorageObject.length; i++) {
                    var tempObject = {};
                    for(var j = 0; j < tableFields.length; j++) {
                        if(tableFields[j] == "id") {
                            tempObject[tableFields[j]] = dropboxStorageObject[i].getId();
                        } else {
                            tempObject[tableFields[j]] = dropboxStorageObject[i].get(tableFields[j]);
                        }
                    }
                    remoteStorageObject.push(tempObject);
                }
                window.localStorage.setItem(datastoreTableName, JSON.stringify(remoteStorageObject));
            }
        }
   
        /*
        * Upload all local data that has no Dropbox unique id to the Dropbox cloud
        *
        * @param string datastoreTableName
        * @return
        */
        this.syncAllUp = function(datastoreTableName) {
            if(this.dropboxClient == null) {
                return;
            }
            var localStorageObject = JSON.parse(window.localStorage.getItem(datastoreTableName));
            if(this.datastoreTable[datastoreTableName] != null) {
                for(var i = 0; i < localStorageObject.length; i++) {
                    if(localStorageObject[i].id === undefined || localStorageObject[i].id === null) {
                        var dropboxObject = this.datastoreTable[datastoreTableName].insert(localStorageObject[i]);
                        localStorageObject[i].id = dropboxObject.getId();
                    }
                }
            }
            window.localStorage.setItem(datastoreTableName, JSON.stringify(localStorageObject));
        }
   
        /*
        * Set the active datastore and get all the listed datastore tables
        *
        * @param DatastoreManager datastoreManager
        * @param Array[string] datastoreTableNames
        * @return
        */
        this.setDatastoreManager = function(datastoreManager, datastoreTableNames) {
            var deferred = $q.defer();
            var global = this;
            this.datastoreManager = datastoreManager;
            this.datastoreManager.openDefaultDatastore(function (error, datastore) {
                if(error) {
                    deferred.reject("Could not open datastore");
                }
                for(var i = 0; i < datastoreTableNames.length; i++) {
                    global.datastoreTable[datastoreTableNames[i]] = datastore.getTable(datastoreTableNames[i]);
                }
                deferred.resolve("Success");
            });
            return deferred.promise;
        }
   
        /*
        * Delete a record from the Dropbox datastore table using the record id
        *
        * @param string datastoreTableName
        * @param object item
        * @return
        */
        this.deleteById = function(datastoreTableName, item) {
            if(this.dropboxClient == null) {
                return;
            }
            if(this.datastoreTable[datastoreTableName] != null) {
                if(item.id !== undefined && item.id !== null) {
                    var record = this.datastoreTable[datastoreTableName].get(item.id);
                    if(record !== null) {
                        record.deleteRecord();
                    }
                }
            }
        }
   
        /*
        * Determine if a string starts with the string included as a parameter
        *
        * @param string str
        * @return boolean
        */
        if(typeof String.prototype.startsWith != "function") {
            String.prototype.startsWith = function (str){
                return this.indexOf(str) == 0;
            };
        }
   
    });


To use this service, add DropboxService as one of your dependencies and take note of the following
methods:

Useful DropboxService MethodsJavaScript
    DropboxService.init("YOUR_APP_KEY_HERE");
    DropboxService.connect(Array[string] tableNames).then(success, error);
    DropboxService.sync(tableName, ["id", "firstname", "lastname"]);
    DropboxService.syncAllUp(tableName);
    DropboxService.deleteById(tableName, recordObject);


My DropboxService is very dependent on having your window.localStorage object matching the Dropbox datastore table. I suggest calling init and connect from your .run() method, while calling the others from a controller. A simple example using the service can be seen below:

DropboxService ExampleJavaScript
    var myExampleApp = angular.module('ionicexample', ['ionic'])
        .run(function($ionicPlatform, DropboxService) {
            $ionicPlatform.ready(function() {
                if(window.localStorage.getItem("tblItems") === undefined || window.localStorage.getItem("tbl_items") === null) {
                    window.localStorage.setItem("tblItems", "[]");
                }
                DropboxService.init("YOUR_KEY_HERE");
                DropboxService.connect(["tblItems"]).then(function(response) {}, function(error) {
                    alert("ERROR: " + error);
                });
            });
        });
   
    myExampleApp.controller("ExampleController", function($scope, DropboxService) {
   
        $scope.init = function() {
            var myItems = JSON.parse(window.localStorage.getItem("tblItems"));
            myItems.push({"item_name": "Super Potion", "item_count": "36"});
            window.localStorage.setItem("tblItems", JSON.stringify(myItems));
        }
   
        $scope.sync = function() {
            DropboxService.sync("tblItems", ["id", "item_name", "item_count"]);
        }
   
    });

   
You can sync data (not files) across all your devices in a similar manner. Do note that
if you plan to have people test or use your application with Dropbox support, you either need to
add them as a test user in the Dropbox developer console or publish your Dropbox app to production,
rather than keeping it in developer mode.



You May Also Like

0 comments