Syncing Data With Dropbox Using Ionic Framework

Storing or syncing data remotely is often a need in modern apps. This generation is all about the cloud and how 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 one of a few cloud storage services that happens to have great APIs and documentation.

The following won’t necessarily show you the best way for syncing data in Ionic Framework using Dropbox’s Datastore API, but it will show you how I did it with success.

These instruction 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 use strictly the JavaScript SDK.

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

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. However, this tutorial will work for both platforms.

The next thing we want to do is install the InAppBrowser plugin:

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

At this point, I strongly recommend you become familiar with what our intentions are with this Dropbox API. We are not using the Core Sync API because we have no intention of working with files. Our plans are to work strictly with data and the Datastore API gives us an object based cloud storage option. It does have restrictions in terms of storage space, but for saving app preferences or small pieces of data, there shouldn’t be any issues.

To use the API, you must register your application with Dropbox. During this process, make sure to choose Datastore API. 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. In all honesty, it doesn’t really matter what you list it as, but for this tutorial use what I said.

The final thing we need from the app page is the application key. 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:

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

Now even though we are going to be using the official JavaScript SDK, we still need to implement our own Oauth solution. The Oauth solution bundled with the SDK won’t work because we need to launch in an external browser. This is where the InAppBrowser plugin comes into play.

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. Per the Dropbox documentation, we must use GET /1/oauth2/authorize as our endpoint with a few parameters. This will initialize the process and once complete redirect you to your redirect uri. When the InAppBrowser listener detects the redirect, we will start parsing what we see in the URL. Specifically we are looking for the access token, token type, and uid. If we find those, then the login was successful and 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.

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.

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

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

This is where things can stray from the official documentation. 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. Since we wan’t our mobile applications to function with and without a network connection, we need to be able to store our data locally and only sync when possible. I’ve accomplished this by following 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 is not the most efficient way to do business, but it is definitely an effective way. If you were aiming for efficiency, you could always only pull remote records that don’t exist locally rather than all records. You could 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:

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:

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:

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"]);
    }

});

Just like this you can sync data (not files) across all your devices. It is important to 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.

Nic Raboy

Nic Raboy

Nic Raboy is an advocate of modern web and mobile development technologies. He has experience in Java, JavaScript, Golang and a variety of frameworks such as Angular, NativeScript, and Apache Cordova. Nic writes about his development experiences related to making web and mobile development easier to understand.

Search

Follow Us

Subscribe

Subscribe to my newsletter for monthly tips and tricks on subjects such as mobile, web, and game development.

Subscribe on YouTube

Support This Site