Save Cordova database to Dropbox

How to save your database to Dropbox (and how to restore it) from within a Cordova application – Android only (part 3)

In this third part of our tutorial article about how to save your database to Dropbox (here abbreviated to DBX, in uppercase) from within a Cordova mobile application for Android devices, we’ll concentrate on the specific steps needed to

  • connect to DBX and to save to Dropbox our mysql dump file
  • retrieve it from DBX and use it to fill out our database

this can be useful if our user has uninstalled our application or if for some reason the local backup file has been lost or corrupted.

Creating a Dropbox app

The first thing we have to do is to create an app in the Dropbox dashboard in order to successfully login using Oauth2. A very good tutorial can be found here and I encourage you to read it carefully if you have some doubt. Here I quickly summarize the needed steps:

  1. navigate to https://www.dropbox.com/developers
  2. login into your account 😉
  3. create a new app clicking on the “Create your app” link in the center of the page
  4. choose Dropbox API
  5. decide if you want/need full access to DBX or if you just need to access a single folder created specifically for your app
  6. set a name for your app and click the blue “Create app” button
  7. take note of your app key and your app secret; also notice and remember the redirect URI, which for Oauth2 must be https://www.dropbox.com/1/oauth2/redirect_receiver

Now your DBX app is created and it will allow you to login to DBX from your hybrid mobile application.

Getting Dropbox Javascript SDK

To use the API you have to install the Dropbox JavaScript SDK, a javascript interface to the Dropbox v2 API. To install it, open a command prompt or a terminal window and type the following command:

npm install --save dropbox

Now, if you are familiar with webpack or browserify, you can just use the SDK as explained in the official Github repository. But if you don’t want or just don’t know how to use those tools, go the directory where the SDK has been installed, copy the file Dropbox-sdk.min.js from the dist directory of the package to www/js folder of your app and then link it in your index.html:

<script type="text/javascript" src="js/Dropbox-sdk.min.js"></script>

Now we are finally ready to rock!

Back to the app

Now it’s time to install the last plugin we need to go ahead, cordova-plugin-inappbrowser. Just navigate to your project root directory with your CLI and type:

cordova plugins add cordova-plugin-inappbrowser

Once installed, we have to tweak it a bit. In fact, if we didn’t apply this little fix, after having logged int DBX account, we would remain blocked on a blank page and we’d never be redirected to our app. So open in your code editor the file platforms/android/platform_www/plugins/cordova-plugin-inappbrowser/www/inappbrowser.js. Near the bottom of the script, around the line 110 or so, immediately after this line:

strWindowFeatures = strWindowFeatures || "";

Put the following 3 lines:

if (strUrl == 'https://www.dropbox.com/1/oauth2/redirect_receiver') {
	strWindowFeatures += "location=no,hidden=yes";
}

This way we’re telling inappbrowser to hide itself if the url is the redirect url set in our DBX app.

Allow origin?

Another issue I run into is the error message returned by Dropbox when I tried to save a file the first time. This was the message:

Error: Request has been terminated Possible causes: the network is offline, Origin is not allowed by Access-Control-Allow-Origin, the page is being unloaded, etc.

To make a long story short, just replace your Content-Security-Policy meta in your index.html with this one (or add it, if it is missing):

<meta http-equiv="Content-Security-Policy" content="default-src *; style-src 'self' http://* 'unsafe-inline'; script-src 'self' http://* 'unsafe-inline' 'unsafe-eval'; media-src *" />

The error is gone,and the app runs. Great!

Setting up Dropbox connection

As said above, DBX API uses OAuth2 to manage authentication. So the first thing we have to do is to prepare some stuff to allow our app to login into DBX:

var CLIENT_ID = 'xxxxxxxxxxxxxxx';//we use App key as our client id 
var dbxt; //an object to store our Dropbox connection and its access token 
var dbx = new Dropbox({clientId: CLIENT_ID}); //the starting Dropbox object

Put above lines immediately after the app.initialize  call, before the other functions we have already written to operate on our database. This is not mandatory, of course: you can put everywhere you’re most comfortable with 🙂

UI modifications

To go ahead with our little project, we have now to slightly change our UI. That is, we’ll modify the behavior of the buttons which export and import respectively the database: instead to actually perform these actions, they will just make visible another div with 2 buttons in order to allow the user to choose

  • if export the database only locally or even to DBX
  • if import the database from a local file or from DBX

Changes to the markup

Let’s go to change our index.html first: just replace the buttons section with the following markup:

		<div class="centered">
			<h3>Select a country:</h3>
			<select id="countries">
			</select>
			<a href="#" id="createDB" class="btn btn-primary">Create tables</a>
			<a href="#" id="emptyDB" class="btn btn-primary">Empty database</a>
			<a href="#" id="setExportOptions" class="btn btn-primary">Export database</a>
			<div id="exportOptions" class="impexpOptions">
				<ul>
					<li>
						<a href="#" id="exportDB" class="btn btn-inline btn-primary">Save locally</a>
					</li>
					<li>
						<a href="#" id="exportDbToDropbox" class="btn btn-inline btn-primary">Save to Dropbox</a>
					</li>
				</ul>
			</div>
			<a href="#" id="setImportOptions" class="btn btn-primary">Import database</a>
			<div id="importOptions" class="impexpOptions">
				<ul>
					<li>
						<a href="#" id="importDB" class="btn btn-inline btn-primary">Restore from device</a>
					</li>
					<li>
						<a href="#" id="importDbFromDropbox" class="btn btn-inline btn-primary">Restore from Dropbox</a>
					</li>
				</ul>
			</div>
		</div>

In your index.css file we add some rules:

.btn-inline{
    margin: 10px 0 !important;
    padding: 5px 10px !important;
}
.impexpOptions{
	display: none;
}
.impexpOptions ul{
	padding: 0;
}
.impexpOptions ul li{
	display: inline-block;
}

Changes to the logic

In your index.js file look for the event handlers of our button and replace them with the follwing ones:

	$('#createDB').click(function (e) {
		e.preventDefault();
		createTables();
	});

	$('#emptyDB').click(function (e) {
		e.preventDefault();
		dropTables();
	});

	$('#setExportOptions').click(function (e) {
		e.preventDefault();
		$('#exportOptions').slideToggle(400, function(){
			$('html, body').animate({
				scrollTop: $('#exportOptions').offset().top
			}, 600);
		});
	});

	$('#exportDB').click(function (e) {
		e.preventDefault();
		exportBackup(false);
	});

	$('#exportDbToDropbox').click(function (e) {
		e.preventDefault();
		exportBackup(true);
	});

	$('#setImportOptions').click(function (e) {
		e.preventDefault();
		$('#importOptions').slideToggle(400, function () {
			$('html, body').animate({
				scrollTop: $('#importOptions').offset().top
			}, 600);
		});
	});

	$('#importDB').click(function (e) {
		e.preventDefault();
		importBackup(false);
	});

	$('#importDbFromDropbox').click(function (e) {
		e.preventDefault();
		importBackup(true);
	});

Wow, several things are changed here! So, let’s summarize the changes:

  • we have added 2 event handlers for buttons with id setExportOptions and setImportOptions: here we just show hidden divs with buttons to actually perform actions (notice we have added a callback to scroll the page and always get the 2 new buttons visible)
  • besides the buttons with ids exportDB and importDB  we have added 2 new buttons with ids exportDbToDropbox and importDbFromDropbox
  • calling exportBackup() and importBackup() functions, we have used a boolean parameter we didn’t use before: this parameter is set to false to export the database only locally and to import the database from a local backup file; it’s set to true, instead, if we want to save our backup to our DBX account or if we want to restore our datbase from a remote backup file saved into our DBX account

In fact, we’re going to modify our exportBackup() and importBackup() functions in order to accept such as paramter and acting accordingly to its value.

Connecting to Dropbox

Before to rewrite our functions to integrate DBX code, let’s see examine how we manage  the connection through Oauth2. First the code (excerpt from exportBackup() function):

if (dbxt == null) {
	dbx.authenticateWithCordova(function (accessToken) {
		dbxt = new Dropbox({accessToken: accessToken});
                //your stuff here
} else {
        //your stuff here
}

Let’s analyze this code. First, we check if dbxt is null. What is dbxt? Is a tokenized DBX connection, that is the object we use once authenticated in DBX. As you remember, we have declared this object immediately after the call to app..initialized method. And immediately after having declared this object we had set up another object we use to perform the login to DBX, the dbx object:

var dbxt;
var dbx = new Dropbox({clientId: CLIENT_ID})

So, if we didn’t already established a valid connection to DBX (dbxt is null), then we use the authenticateWithCordova() method of the object dbx to perform the login and get an accessToken which will identify our connection as an authorized connection. This access token will be a property of the dbxt object which will allow us to perform the actions we need to do. If we already have established a connection to DBX, we just go ahead with our stuff 🙂

Uploading files

To export our database to DBX we have to use the filesUpload() and filesDownload() methods of our dbxt object. Let’s see our new exportBackup() function:

function exportBackup(toDropbox) {
	var successFn = function (sql) {
		window.resolveLocalFileSystemURL(cordova.file.dataDirectory, function (dirEntry) {
			dirEntry.getDirectory('/dropboxTestBackup', {create: true}, function (dirEntry) {
				dirEntry.getFile('dropboxTestBackup.sql', {create: true}, function (fileEntry) {
					alert("Create the file: " + fileEntry.name + '. You can find it in your Internal storage at the following path: Android/data/com.example.dropboxTest/files/dropboxTestBackup/');
					writeFile(fileEntry, sql);
					if (toDropbox) {
						if (dbxt == null) {
							dbx.authenticateWithCordova(function (accessToken) {
								dbxt = new Dropbox({accessToken: accessToken});
								dbxt.filesUpload({
									path: '/' + fileEntry.name,
									contents: sql,
									mode: 'overwrite',
									mute: true
								}).then(function (response) {
									alert('Your backup has been successfully uploaded to your Dropbox!')
									console.log(response);
								}).catch(function (error) {
									alert('Error saving file to your Dropbox! You can retry or manually copy the file Android/data/com.webintenerife.qbook/files/qbookBackup/qbook.sql to your Dropbox folder or in another secure place.')
									console.error(error.message);
								});
							}, function (e) {
								console.log("failed Dropbox authentication");
								console.log(e.message);
							});
						} else {
							console.log('export: user already authenticated');
							dbxt.filesUpload({
								path: '/' + fileEntry.name,
								contents: sql,
								mode: 'overwrite',
								mute: true
							}).then(function (response) {
								alert('Your backup has been successfully uploaded to your Dropbox!')
								console.log(response);
							}).catch(function (error) {
								alert('Error saving file to your Dropbox! You can retry or manually copy the file Android/data/com.webintenerife.qbook/files/qbookBackup/qbook.sql to your Dropbox folder or in another secure place.')
								console.log(error.message);
							});
						}
					}
				});
			}, onErrorCreateFile);
		}, onErrorCreateDir);
	};
	cordova.plugins.sqlitePorter.exportDbToSql(db, {
		successFn: successFn
	});
}

filesUpload() function is quite simple. We pass it following parameters:

  • path: the file path we want
  • content: the data to write in the file (this is passed to the callback function successFn() by cordova.plugins.sqlitePorter.exportDbToSql() function)
  • mode: selects what to do if the file already exists (possible values are add, overwrite and update: http://dropbox.github.io/dropbox-sdk-js/global.html#FilesWriteMode).
  • mute: a boolean value to set if we want receive a notification from DBX when the file has been changed (AFAIK, at july 21, 2017 this doesn’t work)

After the upload is complete we alert user the job is done.

Downloading files

And now go to see the importBackup() function:

function importBackup(fromDropbox) {
	if (!fromDropbox) {
		var pathToFile = cordova.file.dataDirectory + '/dropboxTestBackup/dropboxTestBackup.sql';
		window.resolveLocalFileSystemURL(pathToFile, function (fileEntry) {
			fileEntry.file(function (file) {
				var reader = new FileReader();
				reader.onloadend = function (e) {
					var successFn = function () {
						alert('Database restored successfully!');
						loadCountries();
						loadUsers();
					};
					cordova.plugins.sqlitePorter.importSqlToDb(db, this.result, {
						successFn: successFn
					});
				};
				reader.readAsText(file);
			}, onErrorLoadFile);
		}, onErrorLoadFs);
	} else {
		if (dbxt == null) {
			dbx.authenticateWithCordova(function (accessToken) {
				dbxt = new Dropbox({accessToken: accessToken});
				dbxt.filesDownload({
					path: "/dropboxTestBackup.sql"
				}).then(function (response) {
					var reader = new FileReader();
					reader.readAsText(response.fileBlob);
					reader.onloadend = function () {
						var successFn = function () {
							alert('Database restored successfully!');
							loadCountries();
							loadUsers();
						};
						var errorFn = function (e) {
							console.log('error importing db');
							console.log(e.message);
						};
						cordova.plugins.sqlitePorter.importSqlToDb(db, this.result, {
							successFn: successFn,
							errorFn: errorFn
						});
					};
				}).catch(function (error) {
					alert('Error reading file from your Dropbox!');
					console.error(error.message);
				});
			}, function () {
				console.log("Failed to login to your Dropbox.");
			});
		} else {
			dbxt.filesDownload({
				path: "/dropboxTestBackup.sql"
			}).then(function (response) {
				var reader = new FileReader();
				reader.readAsText(response.fileBlob);
				reader.onloadend = function () {
					var successFn = function () {
						alert('Database restored successfully!');
						loadCountries();
						loadUsers();
					};
					var errorFn = function (e) {
						console.log('error importing db');
						console.log(e.message);
					};
					cordova.plugins.sqlitePorter.importSqlToDb(db, this.result, {
						successFn: successFn,
						errorFn: errorFn
					});
				};
			}).catch(function (error) {
				alert('Error reading file from your Dropbox!');
				console.error(error.message);
			});
		}
	}
}

It’s a bit longer but it’s as simple as the previous one. Once we have established a valid connection to DBX, we can download the file we want and pass it to our reader to read it and, if everything is good, to pass it to SQLite Porter plugin to import the content of the file in our database. All done!

I suggest you to follow the tutorial step by step in order to better understand its logic. But for very lazy people I have prepared a small archive they can download clicking the button below. The archive contains only the three main files index.html, index.css, and index.js: just extract it into your www/ directory and answer yes to overwrite existing files.

Application main files

Just remember to replace the fake app key in index.js line 21 with your DBX app_key!

Conclusions

The app is raw, I know, but I hope it can illustrate clearly enough how you can connect to DBX from within your Android Cordova app and how to save/restore a database. Probably you might want to add a spinner or something else to notify the user the app is running while the database is being exported or imported, but this was not crucial for our goal here so I didn’t cover that.

What’s I really don’t like is the way inappbrowser plugin remain stuck on a blank page while it is connecting to DBX, but I still don’t have found a valid workaround. I thought it could be possible open a custom remote page on a server and use Php or ASP to do the dirty job before to come back to the app, but this would insert a third element (an external server) I would like to avoid. Have you some idea? Let me know!

 


 

2 thoughts on “How to save your database to Dropbox (and how to restore it) from within a Cordova application – Android only (part 3)”

Leave a Comment

Your email address will not be published. Required fields are marked *