diff --git a/ChangeLog b/ChangeLog
index 46a23f05..96288fd6 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -26,6 +26,9 @@ jquery loaded after ie.js, should be before.
inside event function varible called event do not exist
(everithing ok on webkit).
+* Added ability to upload files to dropbox.
+
+
2012.12.12, Version 0.1.8
* Added ability to shutdown Cloud Commander
diff --git a/config.json b/config.json
index 2dd79a63..c014fd05 100644
--- a/config.json
+++ b/config.json
@@ -11,6 +11,8 @@
"github_key" : "891c251b925e4e967fa9",
"github_secret" : "afe9bed1e810c5dc44c4c2a953fc6efb1e5b0545",
"dropbox_key" : "0nd3ssnp5fp7tqs",
+ "dropbox_secret" : "r61lxpchmk8l06o",
+ "dropbox_encoded_key" : "DkMz4FYHQTA=|GW6pf2dONkrGvckMwBsl1V1vysrCPktPiUWN7UpDjw==",
"dropbox_chooser_key" : "o7d6llji052vijk",
"logs" : false,
"show_keys_panel" : true,
diff --git a/lib/client/menu.js b/lib/client/menu.js
index ea34ed92..164f4cc6 100644
--- a/lib/client/menu.js
+++ b/lib/client/menu.js
@@ -34,6 +34,24 @@ var CloudCommander, Util, DOM, $;
}
}
+ function getContent(pCallBack){
+ return DOM.getCurrentFileContent(function(pData){
+ var lName = DOM.getCurrentName();
+ if( Util.isObject(pData) ){
+ pData = JSON.stringify(pData, null, 4);
+
+ var lExt = '.json';
+ if( !Util.checkExtension(lName, lExt) )
+ lName += lExt;
+ }
+
+ Util.exec(pCallBack, {
+ data: pData,
+ name: lName
+ });
+ });
+ }
+
/** function return configureation for menu */
function getConfig (){
return{
@@ -76,24 +94,17 @@ var CloudCommander, Util, DOM, $;
'gist': {
name: 'Gist',
callback: function(key, opt){
- DOM.getCurrentFileContent(function(pData){
- var lName = DOM.getCurrentName();
- if( Util.isObject(pData) ){
- pData = JSON.stringify(pData, null, 4);
-
- var lExt = '.json';
- if( !Util.checkExtension(lName, lExt) )
- lName += lExt;
- }
-
- var lGitHub = cloudcmd.GitHub;
+ getContent(function(pParams){
+ var lGitHub = cloudcmd.GitHub,
+ lData = pParams.data,
+ lName = pParams.name;
if('init' in lGitHub)
- lGitHub.createGist(pData, lName);
+ lGitHub.createGist(lData, lName);
else
Util.exec(cloudcmd.GitHub,
function(){
- lGitHub.createGist(pData, lName);
+ lGitHub.createGist(lData, lName);
});
});
@@ -103,28 +114,37 @@ var CloudCommander, Util, DOM, $;
'gdrive': {
name: 'GDrive',
-
callback: function(key, opt){
- DOM.getCurrentFileContent(function(data){
- var lName = DOM.getCurrentName();
-
- if( Util.isObject(data) )
- data = JSON.stringify(data, null, 4);
- var lData = {
- data: data,
- name: lName
- };
-
+ getContent(function(pParams){
var lGDrive = cloudcmd.GDrive;
if('init' in lGDrive)
- lGDrive.init(lData);
+ lGDrive.init(pParams);
else
- Util.exec(cloudcmd.GDrive, lData);
+ Util.exec(cloudcmd.GDrive, pParams);
});
Util.log('Uploading to gdrive...');
}
+ },
+ 'dropbox':{
+ name: 'DropBox',
+ callback: function(key, opt){
+ getContent(function(pParams){
+ var lDropBox = cloudcmd.DropBox,
+ lData = pParams.data,
+ lName = pParams.name;
+
+ if('init' in lDropBox)
+ lDropBox.uploadFile(lData, lName);
+ else
+ Util.exec(lDropBox, function(){
+ cloudcmd.DropBox.uploadFile(lData, lName);
+ });
+ });
+
+ Util.log('Uploading to dropbox...');
+ }
}
}
},
diff --git a/lib/client/storage/_dropbox.js b/lib/client/storage/_dropbox.js
index bedf6c3d..d1c8b9ee 100644
--- a/lib/client/storage/_dropbox.js
+++ b/lib/client/storage/_dropbox.js
@@ -1,113 +1,102 @@
-var CloudCommander, Util, DOM, gapi;
+var CloudCommander, Util, DOM, CloudFunc, Dropbox, cb, Client;
+/* module for work with github */
(function(){
"use strict";
- var cloudcmd = CloudCommander,
- GDrive = {};
-
+ var cloudcmd = CloudCommander,
+ //Client,
+ DropBoxStore = {};
- function authorize(pData){
- /* https://developers.google.com/drive/credentials */
- Util.setTimeout({
- func : function(pCallBack){
- var lCLIENT_ID = '255175681917.apps.googleusercontent.com',
- lSCOPES = 'https://www.googleapis.com/auth/drive',
- lParams = {
- 'client_id' : lCLIENT_ID,
- 'scope' : lSCOPES,
- 'immediate' : true
- };
-
- gapi.auth.authorize(lParams, pCallBack);
- },
-
- callback : function(pAuthResult){
- var lRet;
- if (pAuthResult && !pAuthResult.error){
- uploadFile(pData);
-
- lRet = true;
- }
- return lRet;
- }
- });
- }
+ /* temporary callback function for work with github */
+ cb = function (err, data){ console.log(err || data);};
- function load(pData){
- var lUrl = 'https://apis.google.com/js/client.js';
-
- DOM.jsload(lUrl, function(){
- authorize(pData);
- });
- }
-
- /**
- * Start the file upload.
- *
- * @param {Object} evt Arguments from the file selector.
- */
- function uploadFile(pData) {
- gapi.client.load('drive', 'v2', function() {
- GDrive.uploadFile(pData);
- });
- }
+ /* PRIVATE FUNCTIONS */
/**
- * Insert new file.
- *
- * @param {File} fileData {name, data} File object to read data from.
- * @param {Function} callback Function to call when the request is complete.
+ * function loads dropbox.js
*/
- GDrive.uploadFile = function(pData, callback) {
- var lData = pData.data,
- lName = pData.name,
- boundary = '-------314159265358979323846',
- delimiter = "\r\n--" + boundary + "\r\n",
- close_delim = "\r\n--" + boundary + "--",
-
- contentType = pData.type || 'application/octet-stream',
- metadata = {
- 'title' : lName,
- 'mimeType' : contentType
- },
-
- base64Data = btoa(lData),
-
- multipartRequestBody =
- delimiter +
- 'Content-Type: application/json\r\n\r\n' +
- JSON.stringify(metadata) +
- delimiter +
- 'Content-Type: ' + contentType + '\r\n' +
- 'Content-Transfer-Encoding: base64\r\n' +
- '\r\n' +
- base64Data +
- close_delim;
-
- var request = gapi.client.request({
- 'path': '/upload/drive/v2/files',
- 'method': 'POST',
- 'params': {'uploadType': 'multipart'},
- 'headers': {
- 'Content-Type': 'multipart/mixed; boundary="' + boundary + '"'
- },
-
- 'body': multipartRequestBody
+ function load(pCallBack){
+ console.time('dropbox load');
+
+ var lSrc = '//cdnjs.cloudflare.com/ajax/libs/dropbox.js/0.7.1/dropbox.min.js',
+ lLocal = CloudFunc.LIBDIRCLIENT + 'storage/dropbox/lib/dropbox.min.js',
+ lOnload = function(){
+ console.timeEnd('dropbox load');
+ DOM.Images.hideLoad();
+
+ Util.exec(pCallBack);
+ };
+
+ DOM.jsload(lSrc, {
+ onload : lOnload,
+ error : DOM.retJSLoad(lLocal, lOnload)
});
- if (!callback)
- callback = function(file) {
- console.log(file);
+ }
+
+ function getUserData(pCallBack){
+ var lName,
+ lShowUserInfo = function(pError, pData){
+ if(!pError){
+ lName = pData.name;
+ console.log('Hello ' + lName + ' :)!');
+ }
};
- request.execute(callback);
+ Client.getUserInfo(lShowUserInfo);
+
+ Util.exec(pCallBack);
+ }
+ /**
+ * function logins on dropbox
+ *
+ * @param pData = {key, secret}
+ */
+ DropBoxStore.login = function(pCallBack){
+ cloudcmd.getConfig(function(pConfig){
+ Client = new Dropbox.Client({
+ key: pConfig.dropbox_encoded_key
+ });
+ Client.authDriver(new Dropbox.Drivers.Redirect({rememberUser: true}));
+
+ Client.authenticate(function(pError, pClient) {
+ Util.log(pError);
+ Client = pClient;
+ Util.exec(pCallBack);
+ });
+
+ });
+
+ };
+ /**
+ * upload file to DropBox
+ */
+ DropBoxStore.uploadFile = function(pContent, pFileName){
+ if(pContent){
+ DOM.Images.showLoad();
+ if(!pFileName)
+ pFileName = Util.getDate();
+
+ Client.writeFile(pFileName, pContent, function(pError, pData){
+ DOM.Images.hideLoad();
+ console.log(pError || pData);
+ });
+ }
+
+ return pContent;
};
-
- GDrive.init = function(pData){
- load(pData);
+ DropBoxStore.init = function(pCallBack){
+ Util.loadOnLoad([
+ Util.retExec(pCallBack),
+ getUserData,
+ DropBoxStore.login,
+ load
+ ]);
+
+ cloudcmd.DropBox.init = null;
};
- cloudcmd.GDrive = GDrive;
-})();
\ No newline at end of file
+ cloudcmd.DropBox = DropBoxStore;
+})();
diff --git a/lib/client/storage/_dropbox_chooser.js b/lib/client/storage/_dropbox_chooser.js
new file mode 100644
index 00000000..5532e1a9
--- /dev/null
+++ b/lib/client/storage/_dropbox_chooser.js
@@ -0,0 +1,55 @@
+var CloudCommander, DOM, Dropbox;
+/* module for work with github */
+
+(function(){
+ "use strict";
+
+ var cloudcmd = CloudCommander,
+ CHOOSER_API = 'https://www.dropbox.com/static/api/1/dropbox.js',
+ CLIENT_ID,
+ DropBoxStore = {},
+ options = {
+ linkType: "direct",
+ success: function(files) {
+ console.log("Here's the file link:" + files[0].link);
+ },
+ cancel: function() {
+ console.log('Chose something');
+ }
+ };
+
+ /* PRIVATE FUNCTIONS */
+
+ /**
+ * function loads dropbox.js
+ */
+ function load(){
+ console.time('dropbox load');
+
+ cloudcmd.getConfig(function(pConfig){
+ var lElement = DOM.anyload({
+ src : CHOOSER_API,
+ not_append : true,
+ id : 'dropboxjs',
+ func : DropBoxStore.choose
+
+ });
+
+ var lDropBoxId = pConfig.dropbox_chooser_key;
+ lElement.setAttribute('data-app-key', lDropBoxId);
+ document.body.appendChild(lElement);
+
+ console.timeEnd('dropbox load');
+ });
+ }
+
+ DropBoxStore.choose = function(){
+ Dropbox.choose(options);
+ };
+
+ DropBoxStore.init = function(){
+ load();
+ };
+
+ cloudcmd.DropBox = DropBoxStore;
+})();
diff --git a/lib/client/storage/_gdrive.js b/lib/client/storage/_gdrive.js
new file mode 100644
index 00000000..bedf6c3d
--- /dev/null
+++ b/lib/client/storage/_gdrive.js
@@ -0,0 +1,113 @@
+var CloudCommander, Util, DOM, gapi;
+
+(function(){
+ "use strict";
+
+ var cloudcmd = CloudCommander,
+ GDrive = {};
+
+
+ function authorize(pData){
+ /* https://developers.google.com/drive/credentials */
+ Util.setTimeout({
+ func : function(pCallBack){
+ var lCLIENT_ID = '255175681917.apps.googleusercontent.com',
+ lSCOPES = 'https://www.googleapis.com/auth/drive',
+ lParams = {
+ 'client_id' : lCLIENT_ID,
+ 'scope' : lSCOPES,
+ 'immediate' : true
+ };
+
+ gapi.auth.authorize(lParams, pCallBack);
+ },
+
+ callback : function(pAuthResult){
+ var lRet;
+ if (pAuthResult && !pAuthResult.error){
+ uploadFile(pData);
+
+ lRet = true;
+ }
+ return lRet;
+ }
+ });
+ }
+
+ function load(pData){
+ var lUrl = 'https://apis.google.com/js/client.js';
+
+ DOM.jsload(lUrl, function(){
+ authorize(pData);
+ });
+ }
+
+ /**
+ * Start the file upload.
+ *
+ * @param {Object} evt Arguments from the file selector.
+ */
+ function uploadFile(pData) {
+ gapi.client.load('drive', 'v2', function() {
+ GDrive.uploadFile(pData);
+ });
+ }
+
+ /**
+ * Insert new file.
+ *
+ * @param {File} fileData {name, data} File object to read data from.
+ * @param {Function} callback Function to call when the request is complete.
+ */
+ GDrive.uploadFile = function(pData, callback) {
+ var lData = pData.data,
+ lName = pData.name,
+ boundary = '-------314159265358979323846',
+ delimiter = "\r\n--" + boundary + "\r\n",
+ close_delim = "\r\n--" + boundary + "--",
+
+ contentType = pData.type || 'application/octet-stream',
+ metadata = {
+ 'title' : lName,
+ 'mimeType' : contentType
+ },
+
+ base64Data = btoa(lData),
+
+ multipartRequestBody =
+ delimiter +
+ 'Content-Type: application/json\r\n\r\n' +
+ JSON.stringify(metadata) +
+ delimiter +
+ 'Content-Type: ' + contentType + '\r\n' +
+ 'Content-Transfer-Encoding: base64\r\n' +
+ '\r\n' +
+ base64Data +
+ close_delim;
+
+ var request = gapi.client.request({
+ 'path': '/upload/drive/v2/files',
+ 'method': 'POST',
+ 'params': {'uploadType': 'multipart'},
+ 'headers': {
+ 'Content-Type': 'multipart/mixed; boundary="' + boundary + '"'
+ },
+
+ 'body': multipartRequestBody
+ });
+
+ if (!callback)
+ callback = function(file) {
+ console.log(file);
+ };
+
+ request.execute(callback);
+ };
+
+
+ GDrive.init = function(pData){
+ load(pData);
+ };
+
+ cloudcmd.GDrive = GDrive;
+})();
\ No newline at end of file
diff --git a/lib/client/storage/_github.js b/lib/client/storage/_github.js
index 24f426f3..0f7ba500 100644
--- a/lib/client/storage/_github.js
+++ b/lib/client/storage/_github.js
@@ -62,11 +62,7 @@ var CloudCommander, Util, DOM, $, Github, cb;
if ( Util.isContainStr(lCode, '?code=') ){
lCode = lCode.replace('?code=','');
- DOM.ajax({
- type : 'put',
- url : AuthURL,
- data: lCode,
- success: function(pData){
+ var lSuccess = function(pData){
if(pData && pData.token){
lToken = pData.token;
@@ -76,8 +72,15 @@ var CloudCommander, Util, DOM, $, Github, cb;
}
else
Util.log("Worning: token not getted...");
- }
- });
+ },
+ lData = {
+ type : 'put',
+ url : AuthURL,
+ data : lCode,
+ success : lSuccess
+ };
+
+ DOM.ajax(lData);
}
else
//window.open('welcome.html', 'welcome','width=300,height=200,menubar=yes,status=yes')">
diff --git a/lib/client/storage/dropbox/LICENSE.txt b/lib/client/storage/dropbox/LICENSE.txt
new file mode 100644
index 00000000..03350194
--- /dev/null
+++ b/lib/client/storage/dropbox/LICENSE.txt
@@ -0,0 +1,19 @@
+Copyright (c) 2012 Dropbox, Inc., http://www.dropbox.com
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/lib/client/storage/dropbox/README.md b/lib/client/storage/dropbox/README.md
new file mode 100644
index 00000000..cbca9f8b
--- /dev/null
+++ b/lib/client/storage/dropbox/README.md
@@ -0,0 +1,73 @@
+# Client Library for the Dropbox API
+
+This is a JavaScript client library for the Dropbox API,
+[written in CoffeeScript](https://github.com/dropbox/dropbox-js/blob/master/doc/coffee_faq.md),
+suitable for use in both modern browsers and in server-side code running under
+[node.js](http://nodejs.org/).
+
+
+## Supported Platforms
+
+This library is tested against the following JavaScript platforms
+
+* [node.js](http://nodejs.org/) 0.6 and 0.8
+* [Chrome](https://www.google.com/chrome) 23
+* [Firefox](www.mozilla.org/firefox) 17
+* Internet Explorer 9
+
+Keep in mind that the versions above are not hard requirements.
+
+
+## Installation and Usage
+
+The
+[getting started guide](https://github.com/dropbox/dropbox-js/blob/master/doc/getting_started.md)
+will help you get your first dropbox.js application up and running.
+
+Peruse the source code of the
+[sample apps](https://github.com/dropbox/dropbox-js/tree/master/samples),
+and borrow as much as you need.
+
+The
+[dropbox.js API reference](http://coffeedoc.info/github/dropbox/dropbox-js/master/class_index.html)
+can be a good place to bookmark while building your application.
+
+If you run into a problem, take a look at
+[the dropbox.js GitHub issue list](https://github.com/dropbox/dropbox-js/issues).
+Please open a new issue if your problem wasn't already reported.
+
+
+## Development
+
+This library is written in CoffeeScript.
+[This document](https://github.com/dropbox/dropbox-js/blob/master/doc/coffee_faq.md)
+can help you understand if that matters to you.
+
+The
+[development guide](https://github.com/dropbox/dropbox-js/blob/master/doc/development.md)
+will make your life easier if you need to change the source code.
+
+
+## Platform-Specific Issues
+
+This lists the most serious problems that you might run into while using
+`dropbox.js`. See
+[the GitHub issue list](https://github.com/dropbox/dropbox-js/issues) for a
+full list of outstanding problems.
+
+### node.js
+
+Reading and writing binary files is currently broken.
+
+### Internet Explorer 9
+
+The library only works when used from `https://` pages, due to
+[these issues](http://blogs.msdn.com/b/ieinternals/archive/2010/05/13/xdomainrequest-restrictions-limitations-and-workarounds.aspx).
+
+Reading and writing binary files is unsupported.
+
+
+## Copyright and License
+
+The library is Copyright (c) 2012 Dropbox Inc., and distributed under the MIT
+License.
diff --git a/lib/client/storage/dropbox/doc/auth_drivers.md b/lib/client/storage/dropbox/doc/auth_drivers.md
new file mode 100644
index 00000000..5507f058
--- /dev/null
+++ b/lib/client/storage/dropbox/doc/auth_drivers.md
@@ -0,0 +1,167 @@
+# Authentication Drivers
+
+This document explains the structure and functionality of a `dropbox.js` OAuth
+driver, and describes the drivers that ship with the library.
+
+## The OAuth Driver Interface
+
+An OAuth driver is a JavaScript object that implements the methods documented
+in the
+[Dropbox.AuthDriver class](http://coffeedoc.info/github/dropbox/dropbox-js/master/classes/Dropbox/AuthDriver.html).
+This class exists solely for the purpose of documenting these methods.
+
+A simple driver can get away with implementing `url` and `doAuthorize`. The
+following example shows an awfully unusable node.js driver that asks the user
+to visit the authorization URL in a browser.
+
+```javascript
+var util = require("util");
+var simpleDriver = {
+ url: function() { return ""; },
+ doAuthorize: function(authUrl, token, tokenSecret, callback) {
+ util.print("Visit the following in a browser, then press Enter\n" +
+ authUrl + "\n");
+ var onEnterKey = function() {
+ process.stdin.removeListener("data", onEnterKey);
+ callback(token);
+ }
+ process.stdin.addListener("data", onEnterKey);
+ process.stdin.resume();
+ }
+};
+```
+
+Complex drivers can take control of the OAuth process by implementing
+`onAuthStateChange`. Implementations of this method should read the `authState`
+field of the `Dropbox.Client` instance they are given to make decisions.
+Implementations should call the `credentials` and `setCredentials` methods on
+the client to control the OAuth process.
+
+See the
+[Dropbox.Drivers.Redirect source](https://github.com/dropbox/dropbox-js/blob/master/src/drivers.coffee)
+for a sample implementation of `onAuthStateChange`.
+
+
+### The OAuth Process Steps
+
+The `authenticate` method in `Dropbox.Client` implements the OAuth process as a
+finite state machine (FSM). The current state is available in the `authState`
+field.
+
+The authentication FSM has the following states.
+
+* `Dropbox.Client.RESET` is the initial state, where the client has no OAuth
+tokens; after `onAuthStateChange` is triggered, the client will attempt to
+obtain an OAuth request token
+* `Dropbox.Client.REQUEST` indicates that the client has obtained an OAuth
+request token; after `onAuthStateChange` is triggered, the client will call
+`doAuthorize` on the OAuth driver, to get the OAuth request token authorized by
+the user
+* `Dropbox.Client.AUTHORIZED` is reached after the `doAuthorize` calls its
+callback, indicating that the user has authorized the OAuth request token;
+after `onAuthStateChange` is triggered, the client will attempt to exchange the
+request token for an OAuth access token
+* `Dropbox.Client.DONE` indicates that the OAuth process has completed, and the
+client has an OAuth access token that can be used in API calls; after
+`onAuthStateChange` is triggered, `authorize` will call its callback function,
+and report success
+* `Dropbox.Client.SIGNED_OFF` is reached when the client's `signOut` method is
+called, after the API call succeeds; after `onAuthStateChange` is triggered,
+`signOut` will call its callback function, and report success
+* `Dropbox.Client.ERROR` is reached if any of the Dropbox API calls used by
+`authorize` or `signOut` results in an error; after `onAuthStateChange` is
+triggered, `authorize` or `signOut` will call its callback function and report
+the error
+
+
+## Built-in OAuth Drivers
+
+`dropbox.js` ships with the OAuth drivers below.
+
+### Dropbox.Drivers.Redirect
+
+The recommended built-in driver for browser applications completes the OAuth
+token authorization step by redirecting the browser to the Dropbox page that
+performs the authorization and having that page redirect back to the
+application page.
+
+This driver's constructor takes the following options.
+
+* `useQuery` should be set to true for applications that use the URL fragment
+(the part after `#`) to store state information
+* `rememberUser` can be set to true to have the driver store the user's OAuth
+token in `localStorage`, so the user doesn't have to authorize the application
+on every request
+
+Although it seems that `rememberUser` should be true by default, it brings a
+couple of drawbacks. The user's token will still be valid after signing off the
+Dropbox web site, so your application will still recognize the user and access
+their Dropbox. This behavior is unintuitive to users. A reasonable compromise
+for apps that use `rememberUser` is to provide a `Sign out` button that calls
+the `signOut` method on the app's `Dropbox.Client` instance.
+
+The
+[checkbox.js](https://github.com/dropbox/dropbox-js/tree/master/samples/checkbox.js)
+sample application uses `rememberUser`, and implements signing off as described
+above.
+
+
+### Dropbox.Drivers.Popup
+
+This driver may be useful for browser applications that can't handle the
+redirections peformed by `Dropbox.Drivers.Redirect`. This driver avoids
+changing the location of the application's browser window by popping up a
+separate window, and loading the Dropbox authorization page in that window.
+
+The popup method has a couple of serious drawbacks. Most browsers will not
+display the popup window by default, and instead will show a hard-to-notice
+warning that the user must interact with to display the popup. The driver's
+code for communicating between the popup and the main application window does
+not work on IE9 and below, so applications that use it will only work on
+Chrome, Firefox and IE10+.
+
+If the drawbacks above are more acceptable than restructuring your application
+to handle redirects, create a page on your site that contains the
+[receiver code](https://github.com/dropbox/dropbox-js/blob/master/test/html/oauth_receiver.html),
+and point the `Dropbox.Drivers.Popup` constructor to it.
+
+```javascript
+client.authDriver(new Dropbox.Drivers.Popup({receiverUrl: "https://url.to/receiver.html"}));
+```
+
+The popup driver adds a `#` (fragment hash) to the receiver URL if necessary,
+to ensure that the user's Dropbox uid and OAuth token are passed to the
+receiver in a URL fragment. This measure may improve your users' privacy, as it
+reduces the chance that their uid or token ends up in a server log.
+
+If you have a good reason to disable the behavior above, set the `noFragment`
+option to true.
+
+```javascript
+client.authDriver(new Dropbox.Drivers.Popup({receiverUrl: "https://url.to/receiver.html", noFragment: true}));
+```
+
+
+### Dropbox.Drivers.NodeServer
+
+This driver is designed for use in the automated test suites of node.js
+applications. It completes the OAuth token authorization step by opening the
+Dropbox authorization page in a new browser window, and "catches" the OAuth
+redirection by setting up a small server using the `https` built-in node.js
+library.
+
+The driver's constructor takes the following options.
+
+* `port` is the HTTP port number; the default is 8192, and works well with the
+Chrome extension described below
+* `favicon` is a path to a file that will be served in response to requests to
+`/favicon.ico`; setting this to a proper image will avoid some warnings in the
+browsers' consoles
+
+To fully automate your test suite, you need to load up the Chrome extension
+bundled in the `dropbox.js` source tree. The extension automatically clicks on
+the "Authorize" button in the Dropbox token authorization page, and closes the
+page after the token authorization process completes. Follow the steps in the
+[development guide](https://github.com/dropbox/dropbox-js/blob/master/doc/development.md)
+to build and install the extension.
+
diff --git a/lib/client/storage/dropbox/doc/coffee_faq.md b/lib/client/storage/dropbox/doc/coffee_faq.md
new file mode 100644
index 00000000..b4814244
--- /dev/null
+++ b/lib/client/storage/dropbox/doc/coffee_faq.md
@@ -0,0 +1,84 @@
+# dropbox.js and CoffeeScript FAQ
+
+dropbox.js is written in [CoffeeScript](http://coffeescript.org/), which
+compiles into very readable JavaScript. This document addresses the concerns
+that are commonly raised by JavaScript developers that do not use or wish to
+learn CoffeeScript.
+
+
+## Do I need to learn CoffeeScript to use the library?
+
+**No.**
+
+The examples in the
+[getting started guide](https://github.com/dropbox/dropbox-js/blob/master/doc/getting_started.md)
+are all written in JavaScript. The
+[dropbox.js API reference](http://coffeedoc.info/github/dropbox/dropbox-js/master/class_index.html)
+covers the entire library, so you should not need to read the library source
+code to understand how to use it.
+
+_Please open an issue if the documentation is unclear!_
+
+The
+[sample apps](https://github.com/dropbox/dropbox-js/tree/master/samples),
+are written in CoffeeScript. Please use the `Try CoffeeScript` button on the
+[CoffeeScript](http://coffeescript.org/) home page to quickly compile the
+sample CoffeeScript into very readable JavaScript.
+
+
+## Do I need to learn CoffeeScript to know how dropbox.js works?
+
+**No.**
+
+You can follow the
+[development guide](https://github.com/dropbox/dropbox-js/blob/master/doc/development.md)
+to build the un-minified JavaScript library in `lib/dropbox.js` and then use
+your editor's find feature to get to the source code for the methods that you
+are interested in.
+
+The building instructions in the development guide do not require familiarity
+with CoffeeScript.
+
+
+## Do I need to learn CoffeeScript to modify dropbox.js?
+
+**Yes, but you might not need to modify the library.**
+
+You do need to learn CoffeeScript to change the `dropbox.js` source code. At
+the same time, you can take advantage of the library hooks and the dynamic
+nature of the JavaScript language to change the behavior of `dropbox.js`
+without touching the source code.
+
+* You can implement your OAuth strategy.
+* You can add methods to the prototype classes such as `Dropbox.Client` to
+implement custom operations. _Please open an issue if you think your addition
+is generally useful!_
+* You can replace internal classes such as `Dropbox.Xhr` (or selectively
+replace methods) with wrappers that tweak the original behavior
+
+
+## Can I contribute to dropbox.js without learning CoffeeScript?
+
+**Yes.**
+
+Most of the development time is spent on API design, developing tests,
+documentation and sample code. Contributing a good testing strategy with a bug
+report can save us 90% of the development time. A feature request that also
+includes a well thought-out API change proposal and testing strategy can also
+save us 90-95% of the implementation time.
+
+At the same time, _please open issues for bugs and feature requests even if you
+don't have time to include any of the above_. Knowing of a problem is the first
+step towards fixing it.
+
+Last, please share your constructive suggestions on how to make `dropbox.js`
+easier to use for JavaScript developers that don't speak CoffeeScript.
+
+
+## Can I complain to get dropbox.js to switch away from CoffeeScript?
+
+**No.**
+
+At the moment, 100% of the library's development comes from unpaid, voluntary
+efforts. Switching to JavaScript would reduce the efficiency of these efforts,
+and it would kill developer motivation.
diff --git a/lib/client/storage/dropbox/doc/development.md b/lib/client/storage/dropbox/doc/development.md
new file mode 100644
index 00000000..1429d64b
--- /dev/null
+++ b/lib/client/storage/dropbox/doc/development.md
@@ -0,0 +1,89 @@
+# dropbox.js Development
+
+Read this document if you want to build `dropbox.js` or modify its source code.
+If you want to write applications using dropbox.js, check out the
+[Getting Started](getting_started.md).
+
+The library is written using [CoffeeScript](http://coffeescript.org/), built
+using [cake](http://coffeescript.org/documentation/docs/cake.html), minified
+using [uglify.js](https://github.com/mishoo/UglifyJS/), tested using
+[mocha](http://visionmedia.github.com/mocha/) and
+[chai.js](http://chaijs.com/), and packaged using [npm](https://npmjs.org/).
+
+If you don't "speak" CoffeeScript,
+[this document](https://github.com/dropbox/dropbox-js/blob/master/doc/coffee_faq.md)
+might address some of your concerns.
+
+
+## Dev Environment Setup
+
+Install [node.js](http://nodejs.org/#download) to get `npm` (the node
+package manager), then use it to install the libraries required by the test
+suite.
+
+```bash
+git clone https://github.com/dropbox/dropbox-js.git
+cd dropbox-js
+npm install
+```
+
+## Build
+
+Run `npm pack` and ignore any deprecation warnings that might come up.
+
+```bash
+npm pack
+```
+
+The build output is in the `lib/` directory. `dropbox.js` is the compiled
+library that ships in the npm package, and `dropbox.min.js` is a minified
+version, optimized for browser apps.
+
+
+## Test
+
+First, you will need to obtain a couple of Dropbox tokens that will be used by
+the automated tests.
+
+```bash
+cake tokens
+```
+
+Re-run the command above if the tests fail due to authentication errors.
+
+Once you have Dropbox tokens, you can run the test suite in node.js or in your
+default browser.
+
+```bash
+cake test
+cake webtest
+```
+
+The library is automatically re-built when running tests, so you don't need to
+run `npm pack`. Please run the tests in both node.js and a browser before
+submitting pull requests.
+
+The tests store all their data in folders named along the lines of
+`js tests.0.ac1n6lgs0e3lerk9`. If tests fail, you might have to clean up these
+folders yourself.
+
+
+## Testing Chrome Extension
+
+The test suite opens up a couple of Dropbox authorization pages, and a page
+that cannot close itself. dropbox.js ships with a Google Chrome extension that
+can fully automate the testing process on Chrome.
+
+The extension is written in CoffeeScript, so you will have to compile it.
+
+```bash
+cake extension
+```
+
+After compilation, have Chrome load the unpacked extension at
+`test/chrome_extension` and click on the scary-looking toolbar icon to activate
+the extension. The icon's color should turn red, to indicate that it is active.
+
+The extension performs some checks to prevent against attacks. However, for
+best results, you should disable the automation (by clicking on the extension
+icon) when you're not testing dropbox.js.
diff --git a/lib/client/storage/dropbox/doc/getting_started.md b/lib/client/storage/dropbox/doc/getting_started.md
new file mode 100644
index 00000000..8fdea598
--- /dev/null
+++ b/lib/client/storage/dropbox/doc/getting_started.md
@@ -0,0 +1,272 @@
+# Getting Started
+
+This is a guide to writing your first dropbox.js application.
+
+
+## Library Setup
+
+This section describes how to get the library hooked up into your application.
+
+### Browser Applications
+
+To get started right away, place this snippet in your page's `
`.
+
+```html
+
+```
+
+The snippet is not a typo. [cdnjs](https://cdnjs.com) recommends using
+[protocol-relative URLs](http://paulirish.com/2010/the-protocol-relative-url/).
+
+To get the latest development build of dropbox.js, follow the steps in the
+[development guide](https://github.com/dropbox/dropbox-js/blob/master/doc/development.md).
+
+
+#### "Powered by Dropbox" Static Web Apps
+
+Before writing any source code, use the
+[console app](https://dl-web.dropbox.com/spa/pjlfdak1tmznswp/powered_by.js/public/index.html)
+to set up your Dropbox. After adding an application, place the source code at
+`/Apps/Static Web Apps/my_awesome_app/public`. You should find a pre-generated
+`index.html` file in there.
+
+### node.js Applications
+
+First, install the `dropbox` [npm](https://npmjs.org/) package.
+
+```bash
+npm install dropbox
+```
+
+Once the npm package is installed, the following `require` statement lets you
+access the same API as browser applications
+
+```javascript
+var Dropbox = require("dropbox");
+```
+
+
+## Initialization
+
+[Register your application](https://www.dropbox.com/developers/apps) to obtain
+an API key. Read the brief
+[API core concepts intro](https://www.dropbox.com/developers/start/core).
+
+Once you have an API key, use it to create a `Dropbox.Client`.
+
+```javascript
+var client = new Dropbox.Client({
+ key: "your-key-here", secret: "your-secret-here", sandbox: true
+});
+```
+
+If your application requires full Dropbox access, leave out the `sandbox: true`
+parameter.
+
+
+### Browser and Open-Source Applications
+
+The Dropbox API guidelines ask that the API key and secret is never exposed in
+cleartext. This is an issue for browser-side and open-source applications.
+
+To meet this requirement,
+[encode your API key](https://dl-web.dropbox.com/spa/pjlfdak1tmznswp/api_keys.js/public/index.html)
+and pass the encoded key string to the `Dropbox.Client` constructor.
+
+```javascript
+var client = new Dropbox.Client({
+ key: "encoded-key-string|it-is-really-really-long", sandbox: true
+});
+```
+
+
+## Authentication
+
+Before you can make any API calls, you need to authenticate your application's
+user with Dropbox, and have them authorize your app's to access their Dropbox.
+
+This process follows the [OAuth 1.0](http://tools.ietf.org/html/rfc5849)
+protocol, which entails sending the user to a Web page on `www.dropbox.com`,
+and then having them redirected back to your application. Each Web application
+has its requirements, so `dropbox.js` lets you customize the authentication
+process by implementing an
+[OAuth driver](https://github.com/dropbox/dropbox-js/blob/master/src/drivers.coffee).
+
+At the same time, dropbox.js ships with a couple of OAuth drivers, and you
+should take advantage of them as you prototype your application.
+
+Read the
+[authentication doc](https://github.com/dropbox/dropbox-js/blob/master/doc/auth_drivers.md)
+for further information about writing an OAuth driver, and to learn about all
+the drivers that ship with `dropbox.js`.
+
+### Browser Setup
+
+The following snippet will set up the recommended driver.
+
+```javascript
+client.authDriver(new Dropbox.Drivers.Redirect());
+```
+
+The
+[authentication doc](https://github.com/dropbox/dropbox-js/blob/master/doc/auth_drivers.md)
+describes some useful options that you can pass to the
+`Dropbox.Drivers.Redirect` constructor.
+
+### node.js Setup
+
+Single-process node.js applications should create one driver to authenticate
+all the clients.
+
+```javascript
+client.authDriver(new Dropbox.Drivers.NodeServer(8191));
+```
+
+The
+[authentication doc](https://github.com/dropbox/dropbox-js/blob/master/doc/auth_drivers.md)
+has useful tips on using the `NodeServer` driver.
+
+### Shared Code
+
+After setting up an OAuth driver, authenticating the user is one method call
+away.
+
+```javascript
+client.authenticate(function(error, client) {
+ if (error) {
+ // Replace with a call to your own error-handling code.
+ //
+ // Don't forget to return from the callback, so you don't execute the code
+ // that assumes everything went well.
+ return showError(error);
+ }
+
+ // Replace with a call to your own application code.
+ //
+ // The user authorized your app, and everything went well.
+ // client is a Dropbox.Client instance that you can use to make API calls.
+ doSomethingCool(client);
+});
+```
+
+## Error Handlng
+
+When Dropbox API calls fail, dropbox.js methods pass a `Dropbox.Error` instance
+as the first parameter in their callbacks. This parameter is named `error` in
+all the code snippets on this page.
+
+If `error` is a truthy value, you should either recover from the error, or
+notify the user that an error occurred. The `status` field in the
+`Dropbox.Error` instance contains the HTTP error code, which should be one of
+the
+[error codes in the REST API](https://www.dropbox.com/developers/reference/api#error-handling).
+
+The snippet below is a template for an extensive error handler.
+
+```javascript
+var showError = function(error) {
+ if (window.console) { // Skip the "if" in node.js code.
+ console.error(error);
+ }
+
+ switch (error.status) {
+ case 401:
+ // If you're using dropbox.js, the only cause behind this error is that
+ // the user token expired.
+ // Get the user through the authentication flow again.
+ break;
+
+ case 404:
+ // The file or folder you tried to access is not in the user's Dropbox.
+ // Handling this error is specific to your application.
+ break;
+
+ case 507:
+ // The user is over their Dropbox quota.
+ // Tell them their Dropbox is full. Refreshing the page won't help.
+ break;
+
+ case 503:
+ // Too many API requests. Tell the user to try again later.
+ // Long-term, optimize your code to use fewer API calls.
+ break;
+
+ case 400: // Bad input parameter
+ case 403: // Bad OAuth request.
+ case 405: // Request method not expected
+ default:
+ // Caused by a bug in dropbox.js, in your application, or in Dropbox.
+ // Tell the user an error occurred, ask them to refresh the page.
+ }
+};
+```
+
+
+## The Fun Part
+
+Authentication was the hard part of the API integration, and error handling was
+the most boring part. Now that these are both behind us, you can interact
+with the user's Dropbox and focus on coding up your application!
+
+The following sections have some commonly used code snippets. The
+[Dropbox.Client API reference](http://coffeedoc.info/github/dropbox/dropbox-js/master/classes/Dropbox/Client.html)
+will help you navigate less common scenarios, and the
+[Dropbox REST API reference](https://www.dropbox.com/developers/reference/api)
+describes the underlying HTTP protocol, and can come in handy when debugging
+your application, or if you want to extend dropbox.js.
+
+### User Info
+
+```javascript
+client.getUserInfo(function(error, userInfo) {
+ if (error) {
+ return showError(error); // Something went wrong.
+ }
+
+ alert("Hello, " + userInfo.name + "!");
+});
+```
+
+### Write a File
+
+```javascript
+client.writeFile("hello_world.txt", "Hello, world!\n", function(error, stat) {
+ if (error) {
+ return showError(error); // Something went wrong.
+ }
+
+ alert("File saved as revision " + stat.revisionTag);
+});
+```
+
+### Read a File
+
+```javascript
+client.readFile("hello_world.txt", function(error, data) {
+ if (error) {
+ return showError(error); // Something went wrong.
+ }
+
+ alert(data); // data has the file's contents
+});
+```
+
+### List a Directory's Contents
+
+```javascript
+client.readdir("/", function(error, entries) {
+ if (error) {
+ return showError(error); // Something went wrong.
+ }
+
+ alert("Your Dropbox contains " + entries.join(", "));
+});
+```
+
+### Sample Applications
+
+Check out the
+[sample apps](https://github.com/dropbox/dropbox-js/tree/master/samples)
+to see how all these concepts play out together.
+
diff --git a/lib/client/storage/dropbox/lib/README.md b/lib/client/storage/dropbox/lib/README.md
new file mode 100644
index 00000000..6a273731
--- /dev/null
+++ b/lib/client/storage/dropbox/lib/README.md
@@ -0,0 +1,10 @@
+# dropbox.js Build Directory
+
+dropbox.js is written in [CoffeeScript](http://coffeescript.org/), and its
+source code is in the `src/` directory. The `lib/` directory contains the
+compiled JavaScript produced by the build process.
+
+The
+[development guide](https://github.com/dropbox/dropbox-js/blob/master/doc/development.md)
+contains a step-by-step guide to the library's build process, and will help you
+populate this directory.
diff --git a/lib/client/storage/dropbox/lib/dropbox.min.js b/lib/client/storage/dropbox/lib/dropbox.min.js
new file mode 100644
index 00000000..86ef3b5f
--- /dev/null
+++ b/lib/client/storage/dropbox/lib/dropbox.min.js
@@ -0,0 +1,5 @@
+/*
+ * dropbox 0.7.1
+ */
+(function(){var t,e,r,n,o,i,s,a,h,u,l,p,c,d,f,y,v,m,g,w,S,b,T={}.hasOwnProperty,E=function(t,e){function r(){this.constructor=t}for(var n in e)T.call(e,n)&&(t[n]=e[n]);return r.prototype=e.prototype,t.prototype=new r,t.__super__=e.prototype,t};if(t=function(){function t(t){this.client=new e(t)}return t}(),t.ApiError=function(){function t(t,e,r){var n;if(this.method=e,this.url=r,this.status=t.status,n=t.responseType?t.response||t.responseText:t.responseText)try{this.responseText=""+n,this.response=JSON.parse(n)}catch(o){this.response=null}else this.responseText="(no response)",this.response=null}return t.prototype.toString=function(){return"Dropbox API error "+this.status+" from "+this.method+" "+this.url+" :: "+this.responseText},t.prototype.inspect=function(){return""+this},t}(),"undefined"!=typeof window&&null!==window?window.atob&&window.btoa?(h=function(t){return window.atob(t)},d=function(t){return window.btoa(t)}):(l="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",f=function(t,e,r){var n,o;for(o=3-e,t<<=8*o,n=3;n>=o;)r.push(l.charAt(63&t>>6*n)),n-=1;for(n=e;3>n;)r.push("="),n+=1;return null},u=function(t,e,r){var n,o;for(o=4-e,t<<=6*o,n=2;n>=o;)r.push(String.fromCharCode(255&t>>8*n)),n-=1;return null},d=function(t){var e,r,n,o,i,s;for(o=[],e=0,r=0,n=i=0,s=t.length;s>=0?s>i:i>s;n=s>=0?++i:--i)e=e<<8|t.charCodeAt(n),r+=1,3===r&&(f(e,r,o),e=r=0);return r>0&&f(e,r,o),o.join("")},h=function(t){var e,r,n,o,i,s,a;for(i=[],e=0,n=0,o=s=0,a=t.length;(a>=0?a>s:s>a)&&(r=t.charAt(o),"="!==r);o=a>=0?++s:--s)e=e<<6|l.indexOf(r),n+=1,4===n&&(u(e,n,i),e=n=0);return n>0&&u(e,n,i),i.join("")}):(h=function(t){var e,r;return e=new Buffer(t,"base64"),function(){var t,n,o;for(o=[],r=t=0,n=e.length;n>=0?n>t:t>n;r=n>=0?++t:--t)o.push(String.fromCharCode(e[r]));return o}().join("")},d=function(t){var e,r;return e=new Buffer(function(){var e,n,o;for(o=[],r=e=0,n=t.length;n>=0?n>e:e>n;r=n>=0?++e:--e)o.push(t.charCodeAt(r));return o}()),e.toString("base64")}),t.Client=function(){function r(e){this.sandbox=e.sandbox||!1,this.apiServer=e.server||this.defaultApiServer(),this.authServer=e.authServer||this.defaultAuthServer(),this.fileServer=e.fileServer||this.defaultFileServer(),this.downloadServer=e.downloadServer||this.defaultDownloadServer(),this.oauth=new t.Oauth(e),this.driver=null,this.filter=null,this.uid=null,this.authState=null,this.authError=null,this._credentials=null,this.setCredentials(e),this.setupUrls()}return r.prototype.authDriver=function(t){return this.driver=t,this},r.prototype.xhrFilter=function(t){return this.filter=t,this},r.prototype.dropboxUid=function(){return this.uid},r.prototype.credentials=function(){return this._credentials||this.computeCredentials(),this._credentials},r.prototype.authenticate=function(r){var n,o,i=this;return n=null,o=function(){var s;if(n!==i.authState&&(n=i.authState,i.driver.onAuthStateChange))return i.driver.onAuthStateChange(i,o);switch(i.authState){case e.RESET:return i.requestToken(function(t,r){var n,s;return t?(i.authError=t,i.authState=e.ERROR):(n=r.oauth_token,s=r.oauth_token_secret,i.oauth.setToken(n,s),i.authState=e.REQUEST),i._credentials=null,o()});case e.REQUEST:return s=i.authorizeUrl(i.oauth.token),i.driver.doAuthorize(s,i.oauth.token,i.oauth.tokenSecret,function(){return i.authState=e.AUTHORIZED,i._credentials=null,o()});case e.AUTHORIZED:return i.getAccessToken(function(t,r){return t?(i.authError=t,i.authState=e.ERROR):(i.oauth.setToken(r.oauth_token,r.oauth_token_secret),i.uid=r.uid,i.authState=e.DONE),i._credentials=null,o()});case e.DONE:return r(null,i);case t.SIGNED_OFF:return i.reset(),o();case e.ERROR:return r(i.authError)}},o(),this},r.prototype.signOut=function(r){var n,o=this;return n=new t.Xhr("POST",this.urls.signOut),n.signWithOauth(this.oauth),this.dispatchXhr(n,function(t){return t?r(t):(o.reset(),o.authState=e.SIGNED_OFF,o.driver.onAuthStateChange?o.driver.onAuthStateChange(o,function(){return r(t)}):r(t))})},r.prototype.signOff=function(t){return this.signOut(t)},r.prototype.getUserInfo=function(e){var r;return r=new t.Xhr("GET",this.urls.accountInfo),r.signWithOauth(this.oauth),this.dispatchXhr(r,function(r,n){return e(r,t.UserInfo.parse(n),n)})},r.prototype.readFile=function(e,r,n){var o,i,s,a,h,u,l;return n||"function"!=typeof r||(n=r,r=null),i={},u=null,o=null,a=null,r&&(r.versionTag?i.rev=r.versionTag:r.rev&&(i.rev=r.rev),r.blob&&(u="blob"),r.binary&&(u="b"),r.length?(null!=r.start?(h=r.start,s=r.start+r.length-1):(h="",s=r.length),a="bytes="+h+"-"+s):null!=r.start&&(a="bytes="+r.start+"-"),r.modifiedSince&&(o=new Date(r.modifiedSince).toUTCString())),l=new t.Xhr("GET",""+this.urls.getFile+"/"+this.urlEncodePath(e)),l.setParams(i).signWithOauth(this.oauth).setResponseType(u),o&&l.setHeader("If-Modified-Since",o),a&&l.setHeader("Range",a),this.dispatchXhr(l,function(e,r,o){return n(e,r,t.Stat.parse(o))})},r.prototype.writeFile=function(e,r,n,o){var i;return o||"function"!=typeof n||(o=n,n=null),i=t.Xhr.canSendForms&&"object"==typeof r,i?this.writeFileUsingForm(e,r,n,o):this.writeFileUsingPut(e,r,n,o)},r.prototype.writeFileUsingForm=function(e,r,n,o){var i,s,a,h;return a=e.lastIndexOf("/"),-1===a?(i=e,e=""):(i=e.substring(a),e=e.substring(0,a)),s={file:i},n&&(n.noOverwrite&&(s.overwrite="false"),n.lastVersionTag?s.parent_rev=n.lastVersionTag:(n.parentRev||n.parent_rev)&&(s.parent_rev=n.parentRev||n.parent_rev)),h=new t.Xhr("POST",""+this.urls.postFile+"/"+this.urlEncodePath(e)),h.setParams(s).signWithOauth(this.oauth).setFileField("file",i,r,"application/octet-stream"),delete s.file,this.dispatchXhr(h,function(e,r){return o(e,t.Stat.parse(r))})},r.prototype.writeFileUsingPut=function(e,r,n,o){var i,s;return i={},n&&(n.noOverwrite&&(i.overwrite="false"),n.lastVersionTag?i.parent_rev=n.lastVersionTag:(n.parentRev||n.parent_rev)&&(i.parent_rev=n.parentRev||n.parent_rev)),s=new t.Xhr("POST",""+this.urls.putFile+"/"+this.urlEncodePath(e)),s.setBody(r).setParams(i).signWithOauth(this.oauth),this.dispatchXhr(s,function(e,r){return o(e,t.Stat.parse(r))})},r.prototype.stat=function(e,r,n){var o,i;return n||"function"!=typeof r||(n=r,r=null),o={},r&&(null!=r.version&&(o.rev=r.version),(r.removed||r.deleted)&&(o.include_deleted="true"),r.readDir&&(o.list="true",r.readDir!==!0&&(o.file_limit=""+r.readDir)),r.cacheHash&&(o.hash=r.cacheHash)),o.include_deleted||(o.include_deleted="false"),o.list||(o.list="false"),i=new t.Xhr("GET",""+this.urls.metadata+"/"+this.urlEncodePath(e)),i.setParams(o).signWithOauth(this.oauth),this.dispatchXhr(i,function(e,r){var o,i,s;return s=t.Stat.parse(r),o=(null!=r?r.contents:void 0)?function(){var e,n,o,s;for(o=r.contents,s=[],e=0,n=o.length;n>e;e++)i=o[e],s.push(t.Stat.parse(i));return s}():void 0,n(e,s,o)})},r.prototype.readdir=function(t,e,r){var n;return r||"function"!=typeof e||(r=e,e=null),n={readDir:!0},e&&(null!=e.limit&&(n.readDir=e.limit),e.versionTag&&(n.versionTag=e.versionTag)),this.stat(t,n,function(t,e,n){var o,i;return o=n?function(){var t,e,r;for(r=[],t=0,e=n.length;e>t;t++)i=n[t],r.push(i.name);return r}():null,r(t,o,e,n)})},r.prototype.metadata=function(t,e,r){return this.stat(t,e,r)},r.prototype.makeUrl=function(e,r,n){var o,i,s,a,h;return n||"function"!=typeof r||(n=r,r=null),i=r&&(r["long"]||r.longUrl||r.downloadHack)?{short_url:"false"}:{},e=this.urlEncodePath(e),s=""+this.urls.shares+"/"+e,o=!1,a=!1,r&&(r.downloadHack?(o=!0,a=!0):r.download&&(o=!0,s=""+this.urls.media+"/"+e)),h=new t.Xhr("POST",s).setParams(i).signWithOauth(this.oauth),this.dispatchXhr(h,function(e,r){return a&&r&&r.url&&(r.url=r.url.replace(this.authServer,this.downloadServer)),n(e,t.PublicUrl.parse(r,o))})},r.prototype.history=function(e,r,n){var o,i;return n||"function"!=typeof r||(n=r,r=null),o={},r&&null!=r.limit&&(o.rev_limit=r.limit),i=new t.Xhr("GET",""+this.urls.revisions+"/"+this.urlEncodePath(e)),i.setParams(o).signWithOauth(this.oauth),this.dispatchXhr(i,function(e,r){var o,i;return i=r?function(){var e,n,i;for(i=[],e=0,n=r.length;n>e;e++)o=r[e],i.push(t.Stat.parse(o));return i}():void 0,n(e,i)})},r.prototype.revisions=function(t,e,r){return this.history(t,e,r)},r.prototype.thumbnailUrl=function(t,e){var r;return r=this.thumbnailXhr(t,e),r.paramsToUrl().url},r.prototype.readThumbnail=function(e,r,n){var o,i;return n||"function"!=typeof r||(n=r,r=null),o="b",r&&r.blob&&(o="blob"),i=this.thumbnailXhr(e,r),i.setResponseType(o),this.dispatchXhr(i,function(e,r,o){return n(e,r,t.Stat.parse(o))})},r.prototype.thumbnailXhr=function(e,r){var n,o;return n={},r&&(r.format?n.format=r.format:r.png&&(n.format="png"),r.size&&(n.size=r.size)),o=new t.Xhr("GET",""+this.urls.thumbnails+"/"+this.urlEncodePath(e)),o.setParams(n).signWithOauth(this.oauth)},r.prototype.revertFile=function(e,r,n){var o;return o=new t.Xhr("POST",""+this.urls.restore+"/"+this.urlEncodePath(e)),o.setParams({rev:r}).signWithOauth(this.oauth),this.dispatchXhr(o,function(e,r){return n(e,t.Stat.parse(r))})},r.prototype.restore=function(t,e,r){return this.revertFile(t,e,r)},r.prototype.findByName=function(e,r,n,o){var i,s;return o||"function"!=typeof n||(o=n,n=null),i={query:r},n&&(null!=n.limit&&(i.file_limit=n.limit),(n.removed||n.deleted)&&(i.include_deleted=!0)),s=new t.Xhr("GET",""+this.urls.search+"/"+this.urlEncodePath(e)),s.setParams(i).signWithOauth(this.oauth),this.dispatchXhr(s,function(e,r){var n,i;return i=r?function(){var e,o,i;for(i=[],e=0,o=r.length;o>e;e++)n=r[e],i.push(t.Stat.parse(n));return i}():void 0,o(e,i)})},r.prototype.search=function(t,e,r,n){return this.findByName(t,e,r,n)},r.prototype.makeCopyReference=function(e,r){var n;return n=new t.Xhr("GET",""+this.urls.copyRef+"/"+this.urlEncodePath(e)),n.signWithOauth(this.oauth),this.dispatchXhr(n,function(e,n){return r(e,t.CopyReference.parse(n))})},r.prototype.copyRef=function(t,e){return this.makeCopyReference(t,e)},r.prototype.pullChanges=function(e,r){var n,o;return r||"function"!=typeof e||(r=e,e=null),n=e?e.cursorTag?{cursor:e.cursorTag}:{cursor:e}:{},o=new t.Xhr("POST",this.urls.delta),o.setParams(n).signWithOauth(this.oauth),this.dispatchXhr(o,function(e,n){return r(e,t.PulledChanges.parse(n))})},r.prototype.delta=function(t,e){return this.pullChanges(t,e)},r.prototype.mkdir=function(e,r){var n;return n=new t.Xhr("POST",this.urls.fileopsCreateFolder),n.setParams({root:this.fileRoot,path:this.normalizePath(e)}).signWithOauth(this.oauth),this.dispatchXhr(n,function(e,n){return r(e,t.Stat.parse(n))})},r.prototype.remove=function(e,r){var n;return n=new t.Xhr("POST",this.urls.fileopsDelete),n.setParams({root:this.fileRoot,path:this.normalizePath(e)}).signWithOauth(this.oauth),this.dispatchXhr(n,function(e,n){return r(e,t.Stat.parse(n))})},r.prototype.unlink=function(t,e){return this.remove(t,e)},r.prototype["delete"]=function(t,e){return this.remove(t,e)},r.prototype.copy=function(e,r,n){var o,i,s;return n||"function"!=typeof o||(n=o,o=null),i={root:this.fileRoot,to_path:this.normalizePath(r)},e instanceof t.CopyReference?i.from_copy_ref=e.tag:i.from_path=this.normalizePath(e),s=new t.Xhr("POST",this.urls.fileopsCopy),s.setParams(i).signWithOauth(this.oauth),this.dispatchXhr(s,function(e,r){return n(e,t.Stat.parse(r))})},r.prototype.move=function(e,r,n){var o,i;return n||"function"!=typeof o||(n=o,o=null),i=new t.Xhr("POST",this.urls.fileopsMove),i.setParams({root:this.fileRoot,from_path:this.normalizePath(e),to_path:this.normalizePath(r)}).signWithOauth(this.oauth),this.dispatchXhr(i,function(e,r){return n(e,t.Stat.parse(r))})},r.prototype.reset=function(){return this.uid=null,this.oauth.setToken(null,""),this.authState=e.RESET,this.authError=null,this._credentials=null,this},r.prototype.setCredentials=function(t){return this.oauth.reset(t),this.uid=t.uid||null,this.authState=t.authState?t.authState:t.token?e.DONE:e.RESET,this.authError=null,this._credentials=null,this},r.prototype.appHash=function(){return this.oauth.appHash()},r.prototype.setupUrls=function(){return this.fileRoot=this.sandbox?"sandbox":"dropbox",this.urls={requestToken:""+this.apiServer+"/1/oauth/request_token",authorize:""+this.authServer+"/1/oauth/authorize",accessToken:""+this.apiServer+"/1/oauth/access_token",signOut:""+this.apiServer+"/1/unlink_access_token",accountInfo:""+this.apiServer+"/1/account/info",getFile:""+this.fileServer+"/1/files/"+this.fileRoot,postFile:""+this.fileServer+"/1/files/"+this.fileRoot,putFile:""+this.fileServer+"/1/files_put/"+this.fileRoot,metadata:""+this.apiServer+"/1/metadata/"+this.fileRoot,delta:""+this.apiServer+"/1/delta",revisions:""+this.apiServer+"/1/revisions/"+this.fileRoot,restore:""+this.apiServer+"/1/restore/"+this.fileRoot,search:""+this.apiServer+"/1/search/"+this.fileRoot,shares:""+this.apiServer+"/1/shares/"+this.fileRoot,media:""+this.apiServer+"/1/media/"+this.fileRoot,copyRef:""+this.apiServer+"/1/copy_ref/"+this.fileRoot,thumbnails:""+this.fileServer+"/1/thumbnails/"+this.fileRoot,fileopsCopy:""+this.apiServer+"/1/fileops/copy",fileopsCreateFolder:""+this.apiServer+"/1/fileops/create_folder",fileopsDelete:""+this.apiServer+"/1/fileops/delete",fileopsMove:""+this.apiServer+"/1/fileops/move"}},r.ERROR=0,r.RESET=1,r.REQUEST=2,r.AUTHORIZED=3,r.DONE=4,r.SIGNED_OFF=5,r.prototype.urlEncodePath=function(e){return t.Xhr.urlEncodeValue(this.normalizePath(e)).replace(/%2F/gi,"/")},r.prototype.normalizePath=function(t){var e;if("/"===t.substring(0,1)){for(e=1;"/"===t.substring(e,e+1);)e+=1;return t.substring(e)}return t},r.prototype.requestToken=function(e){var r;return r=new t.Xhr("POST",this.urls.requestToken).signWithOauth(this.oauth),this.dispatchXhr(r,e)},r.prototype.authorizeUrl=function(e){var r;return r={oauth_token:e,oauth_callback:this.driver.url()},""+this.urls.authorize+"?"+t.Xhr.urlEncode(r)},r.prototype.getAccessToken=function(e){var r;return r=new t.Xhr("POST",this.urls.accessToken).signWithOauth(this.oauth),this.dispatchXhr(r,e)},r.prototype.dispatchXhr=function(t,e){var r;return t.setCallback(e),t.prepare(),r=t.xhr,this.filter&&!this.filter(r,t)?r:(t.send(),r)},r.prototype.defaultApiServer=function(){return"https://api.dropbox.com"},r.prototype.defaultAuthServer=function(){return this.apiServer.replace("api.","www.")},r.prototype.defaultFileServer=function(){return this.apiServer.replace("api.","api-content.")},r.prototype.defaultDownloadServer=function(){return this.apiServer.replace("api.","dl.")},r.prototype.computeCredentials=function(){var t;return t={key:this.oauth.key,sandbox:this.sandbox},this.oauth.secret&&(t.secret=this.oauth.secret),this.oauth.token&&(t.token=this.oauth.token,t.tokenSecret=this.oauth.tokenSecret),this.uid&&(t.uid=this.uid),this.authState!==e.ERROR&&this.authState!==e.RESET&&this.authState!==e.DONE&&this.authState!==e.SIGNED_OFF&&(t.authState=this.authState),this.apiServer!==this.defaultApiServer()&&(t.server=this.apiServer),this.authServer!==this.defaultAuthServer()&&(t.authServer=this.authServer),this.fileServer!==this.defaultFileServer()&&(t.fileServer=this.fileServer),this.downloadServer!==this.defaultDownloadServer()&&(t.downloadServer=this.downloadServer),this._credentials=t},r}(),e=t.Client,t.AuthDriver=function(){function t(){}return t.prototype.url=function(){return"https://some.url"},t.prototype.doAuthorize=function(t,e,r,n){return n("oauth-token")},t.prototype.onAuthStateChange=function(t,e){return e()},t}(),t.Drivers={},t.Drivers.BrowserBase=function(){function t(t){this.rememberUser=(null!=t?t.rememberUser:void 0)||!1,this.scope=(null!=t?t.scope:void 0)||"default"}return t.prototype.onAuthStateChange=function(t,r){var n,o=this;switch(this.setStorageKey(t),t.authState){case e.RESET:return(n=this.loadCredentials())?n.authState?(t.setCredentials(n),r()):this.rememberUser?(t.setCredentials(n),t.getUserInfo(function(e){return e&&(t.reset(),o.forgetCredentials()),r()})):(this.forgetCredentials(),r()):r();case e.REQUEST:return this.storeCredentials(t.credentials()),r();case e.DONE:return this.rememberUser?this.storeCredentials(t.credentials()):this.forgetCredentials(),r();case e.SIGNED_OFF:return this.forgetCredentials(),r();case e.ERROR:return this.forgetCredentials(),r();default:return r()}},t.prototype.setStorageKey=function(t){return this.storageKey="dropbox-auth:"+this.scope+":"+t.appHash(),this},t.prototype.storeCredentials=function(t){return localStorage.setItem(this.storageKey,JSON.stringify(t))},t.prototype.loadCredentials=function(){var t;if(t=localStorage.getItem(this.storageKey),!t)return null;try{return JSON.parse(t)}catch(e){return null}},t.prototype.forgetCredentials=function(){return localStorage.removeItem(this.storageKey)},t.currentLocation=function(){return window.location.href},t}(),t.Drivers.Redirect=function(r){function n(t){n.__super__.constructor.call(this,t),this.useQuery=(null!=t?t.useQuery:void 0)||!1,this.receiverUrl=this.computeUrl(t),this.tokenRe=RegExp("(#|\\?|&)oauth_token=([^]+)(&|#|$)")}return E(n,r),n.prototype.onAuthStateChange=function(t,r){var o;return this.setStorageKey(t),t.authState===e.RESET&&(o=this.loadCredentials())&&o.authState&&(o.token===this.locationToken()&&o.authState===e.REQUEST?(o.authState=e.AUTHORIZED,this.storeCredentials(o)):this.forgetCredentials()),n.__super__.onAuthStateChange.call(this,t,r)},n.prototype.url=function(){return this.receiverUrl},n.prototype.doAuthorize=function(t){return window.location.assign(t)},n.prototype.computeUrl=function(){var e,r,n,o;return o="_dropboxjs_scope="+encodeURIComponent(this.scope),r=t.Drivers.BrowserBase.currentLocation(),-1===r.indexOf("#")?e=null:(n=r.split("#",2),r=n[0],e=n[1]),this.useQuery?r+=-1===r.indexOf("?")?"?"+o:"&"+o:e="?"+o,e?r+"#"+e:r},n.prototype.locationToken=function(){var e,r,n;return e=t.Drivers.BrowserBase.currentLocation(),n="_dropboxjs_scope="+encodeURIComponent(this.scope)+"&",-1===("function"==typeof e.indexOf?e.indexOf(n):void 0)?null:(r=this.tokenRe.exec(e),r?decodeURIComponent(r[2]):null)},n}(t.Drivers.BrowserBase),t.Drivers.Popup=function(r){function n(t){n.__super__.constructor.call(this,t),this.receiverUrl=this.computeUrl(t),this.tokenRe=RegExp("(#|\\?|&)oauth_token=([^]+)(&|#|$)")}return E(n,r),n.prototype.onAuthStateChange=function(t,r){var o;return this.setStorageKey(t),t.authState===e.RESET&&(o=this.loadCredentials())&&o.authState&&this.forgetCredentials(),n.__super__.onAuthStateChange.call(this,t,r)},n.prototype.doAuthorize=function(t,e,r,n){return this.listenForMessage(e,n),this.openWindow(t)},n.prototype.url=function(){return this.receiverUrl},n.prototype.computeUrl=function(e){var r;if(e){if(e.receiverUrl)return e.noFragment||-1!==e.receiverUrl.indexOf("#")?e.receiverUrl:e.receiverUrl+"#";if(e.receiverFile)return r=t.Drivers.BrowserBase.currentLocation().split("/"),r[r.length-1]=e.receiverFile,e.noFragment?r.join("/"):r.join("/")+"#"}return t.Drivers.BrowserBase.currentLocation()},n.prototype.openWindow=function(t){return window.open(t,"_dropboxOauthSigninWindow",this.popupWindowSpec(980,980))},n.prototype.popupWindowSpec=function(t,e){var r,n,o,i,s,a,h,u,l,p;return s=null!=(h=window.screenX)?h:window.screenLeft,a=null!=(u=window.screenY)?u:window.screenTop,i=null!=(l=window.outerWidth)?l:document.documentElement.clientWidth,r=null!=(p=window.outerHeight)?p:document.documentElement.clientHeight,n=Math.round(s+(i-t)/2),o=Math.round(a+(r-e)/2.5),"width="+t+",height="+e+","+("left="+n+",top="+o)+"dialog=yes,dependent=yes,scrollbars=yes,location=yes"},n.prototype.listenForMessage=function(t,e){var r,n;return n=this.tokenRe,r=function(o){var i;return i=n.exec(""+o.data),i&&decodeURIComponent(i[2])===t?(window.removeEventListener("message",r),e()):void 0},window.addEventListener("message",r,!1)},n}(t.Drivers.BrowserBase),t.Drivers.NodeServer=function(){function t(t){this.port=(null!=t?t.port:void 0)||8912,this.faviconFile=(null!=t?t.favicon:void 0)||null,this.fs=require("fs"),this.http=require("http"),this.open=require("open"),this.callbacks={},this.urlRe=RegExp("^/oauth_callback\\?"),this.tokenRe=RegExp("(\\?|&)oauth_token=([^&]+)(&|$)"),this.createApp()}return t.prototype.url=function(){return"http://localhost:"+this.port+"/oauth_callback"},t.prototype.doAuthorize=function(t,e,r,n){return this.callbacks[e]=n,this.openBrowser(t)},t.prototype.openBrowser=function(t){if(!t.match(/^https?:\/\//))throw Error("Not a http/https URL: "+t);return this.open(t)},t.prototype.createApp=function(){var t=this;return this.app=this.http.createServer(function(e,r){return t.doRequest(e,r)}),this.app.listen(this.port)},t.prototype.closeServer=function(){return this.app.close()},t.prototype.doRequest=function(t,e){var r,n,o,i=this;return this.urlRe.exec(t.url)&&(n=this.tokenRe.exec(t.url),n&&(o=decodeURIComponent(n[2]),this.callbacks[o]&&(this.callbacks[o](),delete this.callbacks[o]))),r="",t.on("data",function(t){return r+=t}),t.on("end",function(){return i.faviconFile&&"/favicon.ico"===t.url?i.sendFavicon(e):i.closeBrowser(e)})},t.prototype.closeBrowser=function(t){var e;return e='\n\n
Please close this window.
',t.writeHead(200,{"Content-Length":e.length,"Content-Type":"text/html"}),t.write(e),t.end},t.prototype.sendFavicon=function(t){return this.fs.readFile(this.faviconFile,function(e,r){return t.writeHead(200,{"Content-Length":r.length,"Content-Type":"image/x-icon"}),t.write(r),t.end})},t}(),p=function(t,e){return a(m(S(t),S(e),t.length,e.length))},c=function(t){return a(w(S(t),t.length))},("undefined"==typeof window||null===window)&&(y=require("crypto"),p=function(t,e){var r;return r=y.createHmac("sha1",e),r.update(t),r.digest("base64")},c=function(t){var e;return e=y.createHash("sha1"),e.update(t),e.digest("base64")}),m=function(t,e,r,n){var o,i,s,a;return e.length>16&&(e=w(e,n)),s=function(){var t,r;for(r=[],i=t=0;16>t;i=++t)r.push(909522486^e[i]);return r}(),a=function(){var t,r;for(r=[],i=t=0;16>t;i=++t)r.push(1549556828^e[i]);return r}(),o=w(s.concat(t),64+r),w(a.concat(o),84)},w=function(t,e){var r,n,o,i,a,h,u,l,p,c,d,f,y,v,m,w,S,b;for(t[e>>2]|=1<<31-((3&e)<<3),t[(e+8>>6<<4)+15]=e<<3,w=Array(80),r=1732584193,o=-271733879,a=-1732584194,u=271733878,p=-1009589776,f=0,m=t.length;m>f;){for(n=r,i=o,h=a,l=u,c=p,y=b=0;80>b;y=++b)w[y]=16>y?t[f+y]:g(w[y-3]^w[y-8]^w[y-14]^w[y-16],1),20>y?(d=o&a|~o&u,v=1518500249):40>y?(d=o^a^u,v=1859775393):60>y?(d=o&a|o&u|a&u,v=-1894007588):(d=o^a^u,v=-899497514),S=s(s(g(r,5),d),s(s(p,w[y]),v)),p=u,u=a,a=g(o,30),o=r,r=S;r=s(r,n),o=s(o,i),a=s(a,h),u=s(u,l),p=s(p,c),f+=16}return[r,o,a,u,p]},g=function(t,e){return t<>>32-e},s=function(t,e){var r,n;return n=(65535&t)+(65535&e),r=(t>>16)+(e>>16)+(n>>16),r<<16|65535&n},a=function(t){var e,r,n,o,i;for(o="",e=0,n=4*t.length;n>e;)r=e,i=(255&t[r>>2]>>(3-(3&r)<<3))<<16,r+=1,i|=(255&t[r>>2]>>(3-(3&r)<<3))<<8,r+=1,i|=255&t[r>>2]>>(3-(3&r)<<3),o+=b[63&i>>18],o+=b[63&i>>12],e+=1,o+=e>=n?"=":b[63&i>>6],e+=1,o+=e>=n?"=":b[63&i],e+=1;return o},b="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",S=function(t){var e,r,n,o,i;for(e=[],n=255,r=o=0,i=t.length;i>=0?i>o:o>i;r=i>=0?++o:--o)e[r>>2]|=(t.charCodeAt(r)&n)<<(3-(3&r)<<3);return e},t.Oauth=function(){function e(t){this.key=this.k=null,this.secret=this.s=null,this.token=null,this.tokenSecret=null,this._appHash=null,this.reset(t)}return e.prototype.reset=function(t){var e,r,n,o;if(t.secret)this.k=this.key=t.key,this.s=this.secret=t.secret,this._appHash=null;else if(t.key)this.key=t.key,this.secret=null,n=h(v(this.key).split("|",2)[1]),o=n.split("?",2),e=o[0],r=o[1],this.k=decodeURIComponent(e),this.s=decodeURIComponent(r),this._appHash=null;else if(!this.k)throw Error("No API key supplied");return t.token?this.setToken(t.token,t.tokenSecret):this.setToken(null,"")},e.prototype.setToken=function(e,r){if(e&&!r)throw Error("No secret supplied with the user token");return this.token=e,this.tokenSecret=r||"",this.hmacKey=t.Xhr.urlEncodeValue(this.s)+"&"+t.Xhr.urlEncodeValue(r),null},e.prototype.authHeader=function(e,r,n){var o,i,s,a,h,u;this.addAuthParams(e,r,n),i=[];for(s in n)a=n[s],"oauth_"===s.substring(0,6)&&i.push(s);for(i.sort(),o=[],h=0,u=i.length;u>h;h++)s=i[h],o.push(t.Xhr.urlEncodeValue(s)+'="'+t.Xhr.urlEncodeValue(n[s])+'"'),delete n[s];return"OAuth "+o.join(",")},e.prototype.addAuthParams=function(t,e,r){return this.boilerplateParams(r),r.oauth_signature=this.signature(t,e,r),r},e.prototype.boilerplateParams=function(t){return t.oauth_consumer_key=this.k,t.oauth_nonce=this.nonce(),t.oauth_signature_method="HMAC-SHA1",this.token&&(t.oauth_token=this.token),t.oauth_timestamp=Math.floor(Date.now()/1e3),t.oauth_version="1.0",t},e.prototype.nonce=function(){return Date.now().toString(36)+Math.random().toString(36)},e.prototype.signature=function(e,r,n){var o;return o=e.toUpperCase()+"&"+t.Xhr.urlEncodeValue(r)+"&"+t.Xhr.urlEncodeValue(t.Xhr.urlEncode(n)),p(o,this.hmacKey)},e.prototype.appHash=function(){return this._appHash?this._appHash:this._appHash=c(this.k).replace(/\=/g,"")},e}(),null==Date.now&&(Date.now=function(){return(new Date).getTime()}),v=function(t,e){var r,n,o,i,s,a,u,l,p,c,f,y;for(e?(e=[encodeURIComponent(t),encodeURIComponent(e)].join("?"),t=function(){var e,n,o;for(o=[],r=e=0,n=t.length/2;n>=0?n>e:e>n;r=n>=0?++e:--e)o.push(16*(15&t.charCodeAt(2*r))+(15&t.charCodeAt(2*r+1)));return o}()):(c=t.split("|",2),t=c[0],e=c[1],t=h(t),t=function(){var e,n,o;for(o=[],r=e=0,n=t.length;n>=0?n>e:e>n;r=n>=0?++e:--e)o.push(t.charCodeAt(r));return o}(),e=h(e)),i=function(){for(y=[],l=0;256>l;l++)y.push(l);return y}.apply(this),a=0,s=p=0;256>p;s=++p)a=(a+i[r]+t[s%t.length])%256,f=[i[a],i[s]],i[s]=f[0],i[a]=f[1];return s=a=0,o=function(){var t,r,o,h;for(h=[],u=t=0,r=e.length;r>=0?r>t:t>r;u=r>=0?++t:--t)s=(s+1)%256,a=(a+i[s])%256,o=[i[a],i[s]],i[s]=o[0],i[a]=o[1],n=i[(i[s]+i[a])%256],h.push(String.fromCharCode((n^e.charCodeAt(u))%256));return h}(),t=function(){var e,n,o;for(o=[],r=e=0,n=t.length;n>=0?n>e:e>n;r=n>=0?++e:--e)o.push(String.fromCharCode(t[r]));return o}(),[d(t.join("")),d(o.join(""))].join("|")},t.PulledChanges=function(){function e(e){var r;this.blankSlate=e.reset||!1,this.cursorTag=e.cursor,this.shouldPullAgain=e.has_more,this.shouldBackOff=!this.shouldPullAgain,this.changes=e.cursor&&e.cursor.length?function(){var n,o,i,s;for(i=e.entries,s=[],n=0,o=i.length;o>n;n++)r=i[n],s.push(t.PullChange.parse(r));return s}():[]}return e.parse=function(e){return e&&"object"==typeof e?new t.PulledChanges(e):e},e.prototype.blankSlate=void 0,e.prototype.cursorTag=void 0,e.prototype.changes=void 0,e.prototype.shouldPullAgain=void 0,e.prototype.shouldBackOff=void 0,e}(),t.PullChange=function(){function e(e){this.path=e[0],this.stat=t.Stat.parse(e[1]),this.stat?this.wasRemoved=!1:(this.stat=null,this.wasRemoved=!0)}return e.parse=function(e){return e&&"object"==typeof e?new t.PullChange(e):e},e.prototype.path=void 0,e.prototype.wasRemoved=void 0,e.prototype.stat=void 0,e}(),t.PublicUrl=function(){function e(t,e){this.url=t.url,this.expiresAt=new Date(Date.parse(t.expires)),this.isDirect=e===!0?!0:e===!1?!1:864e5>=Date.now()-this.expiresAt,this.isPreview=!this.isDirect}return e.parse=function(e,r){return e&&"object"==typeof e?new t.PublicUrl(e,r):e},e.prototype.url=void 0,e.prototype.expiresAt=void 0,e.prototype.isDirect=void 0,e.prototype.isPreview=void 0,e}(),t.CopyReference=function(){function e(t){"object"==typeof t?(this.tag=t.copy_ref,this.expiresAt=new Date(Date.parse(t.expires))):(this.tag=t,this.expiresAt=new Date)}return e.parse=function(e){return!e||"object"!=typeof e&&"string"!=typeof e?e:new t.CopyReference(e)},e.prototype.tag=void 0,e.prototype.expiresAt=void 0,e}(),t.Stat=function(){function e(t){var e,r,n,o;switch(this.path=t.path,"/"!==this.path.substring(0,1)&&(this.path="/"+this.path),e=this.path.length-1,e>=0&&"/"===this.path.substring(e)&&(this.path=this.path.substring(0,e)),r=this.path.lastIndexOf("/"),this.name=this.path.substring(r+1),this.isFolder=t.is_dir||!1,this.isFile=!this.isFolder,this.isRemoved=t.is_deleted||!1,this.typeIcon=t.icon,this.modifiedAt=(null!=(n=t.modified)?n.length:void 0)?new Date(Date.parse(t.modified)):null,this.clientModifiedAt=(null!=(o=t.client_mtime)?o.length:void 0)?new Date(Date.parse(t.client_mtime)):null,t.root){case"dropbox":this.inAppFolder=!1;break;case"app_folder":this.inAppFolder=!0;break;default:this.inAppFolder=null}this.size=t.bytes||0,this.humanSize=t.size||"",this.hasThumbnail=t.thumb_exists||!1,this.isFolder?(this.versionTag=t.hash,this.mimeType=t.mime_type||"inode/directory"):(this.versionTag=t.rev,this.mimeType=t.mime_type||"application/octet-stream")}return e.parse=function(e){return e&&"object"==typeof e?new t.Stat(e):e},e.prototype.path=null,e.prototype.name=null,e.prototype.inAppFolder=null,e.prototype.isFolder=null,e.prototype.isFile=null,e.prototype.isRemoved=null,e.prototype.typeIcon=null,e.prototype.versionTag=null,e.prototype.mimeType=null,e.prototype.size=null,e.prototype.humanSize=null,e.prototype.hasThumbnail=null,e.prototype.modifiedAt=null,e.prototype.clientModifiedAt=null,e}(),t.UserInfo=function(){function e(t){var e;this.name=t.display_name,this.email=t.email,this.countryCode=t.country||null,this.uid=""+t.uid,t.public_app_url?(this.publicAppUrl=t.public_app_url,e=this.publicAppUrl.length-1,e>=0&&"/"===this.publicAppUrl.substring(e)&&(this.publicAppUrl=this.publicAppUrl.substring(0,e))):this.publicAppUrl=null,this.referralUrl=t.referral_link,this.quota=t.quota_info.quota,this.privateBytes=t.quota_info.normal||0,this.sharedBytes=t.quota_info.shared||0,this.usedQuota=this.privateBytes+this.sharedBytes}return e.parse=function(e){return e&&"object"==typeof e?new t.UserInfo(e):e},e.prototype.name=null,e.prototype.email=null,e.prototype.countryCode=null,e.prototype.uid=null,e.prototype.referralUrl=null,e.prototype.publicAppUrl=null,e.prototype.quota=null,e.prototype.usedQuota=null,e.prototype.privateBytes=null,e.prototype.sharedBytes=null,e}(),"undefined"!=typeof window&&null!==window?(!window.XDomainRequest||"withCredentials"in new XMLHttpRequest?(i=window.XMLHttpRequest,o=!1,r=-1===window.navigator.userAgent.indexOf("Firefox")):(i=window.XDomainRequest,o=!0,r=!1),n=!0):(i=require("xmlhttprequest").XMLHttpRequest,o=!1,r=!1,n=!1),t.Xhr=function(){function e(t,e){this.method=t,this.isGet="GET"===this.method,this.url=e,this.headers={},this.params=null,this.body=null,this.preflight=!(this.isGet||"POST"===this.method),this.signed=!1,this.responseType=null,this.callback=null,this.xhr=null}return e.Request=i,e.ieMode=o,e.canSendForms=r,e.doesPreflight=n,e.prototype.setParams=function(t){if(this.signed)throw Error("setParams called after addOauthParams or addOauthHeader");if(this.params)throw Error("setParams cannot be called twice");return this.params=t,this},e.prototype.setCallback=function(t){return this.callback=t,this},e.prototype.signWithOauth=function(e){return t.Xhr.ieMode||t.Xhr.doesPreflight&&!this.preflight?this.addOauthParams(e):this.addOauthHeader(e)},e.prototype.addOauthParams=function(t){if(this.signed)throw Error("Request already has an OAuth signature");return this.params||(this.params={}),t.addAuthParams(this.method,this.url,this.params),this.signed=!0,this},e.prototype.addOauthHeader=function(t){if(this.signed)throw Error("Request already has an OAuth signature");return this.params||(this.params={}),this.signed=!0,this.setHeader("Authorization",t.authHeader(this.method,this.url,this.params))},e.prototype.setBody=function(t){if(this.isGet)throw Error("setBody cannot be called on GET requests");if(null!==this.body)throw Error("Request already has a body");return this.preflight||"undefined"!=typeof FormData&&null!==FormData&&t instanceof FormData||(this.preflight=!0),this.body=t,this},e.prototype.setResponseType=function(t){return this.responseType=t,this},e.prototype.setHeader=function(t,e){var r;if(this.headers[t])throw r=this.headers[t],Error("HTTP header "+t+" already set to "+r);if("Content-Type"===t)throw Error("Content-Type is automatically computed based on setBody");return this.preflight=!0,this.headers[t]=e,this},e.prototype.setFileField=function(t,e,r,n){var o,i;if(null!==this.body)throw Error("Request already has a body");
+if(this.isGet)throw Error("paramsToBody cannot be called on GET requests");return i="object"==typeof r&&("undefined"!=typeof Blob&&null!==Blob&&r instanceof Blob||"undefined"!=typeof File&&null!==File&&r instanceof File),i?(this.body=new FormData,this.body.append(t,r,e)):(n||(n="application/octet-stream"),o=this.multipartBoundary(),this.headers["Content-Type"]="multipart/form-data; boundary="+o,this.body=["--",o,"\r\n",'Content-Disposition: form-data; name="',t,'"; filename="',e,'"\r\n',"Content-Type: ",n,"\r\n","Content-Transfer-Encoding: binary\r\n\r\n",r,"\r\n","--",o,"--","\r\n"].join(""))},e.prototype.multipartBoundary=function(){return[Date.now().toString(36),Math.random().toString(36)].join("----")},e.prototype.paramsToUrl=function(){var e;return this.params&&(e=t.Xhr.urlEncode(this.params),0!==e.length&&(this.url=[this.url,"?",e].join("")),this.params=null),this},e.prototype.paramsToBody=function(){if(this.params){if(null!==this.body)throw Error("Request already has a body");if(this.isGet)throw Error("paramsToBody cannot be called on GET requests");this.headers["Content-Type"]="application/x-www-form-urlencoded",this.body=t.Xhr.urlEncode(this.params),this.params=null}return this},e.prototype.prepare=function(){var e,r,n,o,i=this;if(r=t.Xhr.ieMode,this.isGet||null!==this.body||r?(this.paramsToUrl(),null!==this.body&&"string"==typeof this.body&&(this.headers["Content-Type"]="text/plain; charset=utf8")):this.paramsToBody(),this.xhr=new t.Xhr.Request,r?(this.xhr.onload=function(){return i.onLoad()},this.xhr.onerror=function(){return i.onError()}):this.xhr.onreadystatechange=function(){return i.onReadyStateChange()},this.xhr.open(this.method,this.url,!0),!r){o=this.headers;for(e in o)T.call(o,e)&&(n=o[e],this.xhr.setRequestHeader(e,n))}return this.responseType&&("b"===this.responseType?this.xhr.overrideMimeType&&this.xhr.overrideMimeType("text/plain; charset=x-user-defined"):this.xhr.responseType=this.responseType),this},e.prototype.send=function(t){return this.callback=t||this.callback,null!==this.body?this.xhr.send(this.body):this.xhr.send(),this},e.urlEncode=function(t){var e,r,n;e=[];for(r in t)n=t[r],e.push(this.urlEncodeValue(r)+"="+this.urlEncodeValue(n));return e.sort().join("&")},e.urlEncodeValue=function(t){return encodeURIComponent(""+t).replace(/\!/g,"%21").replace(/'/g,"%27").replace(/\(/g,"%28").replace(/\)/g,"%29").replace(/\*/g,"%2A")},e.urlDecode=function(t){var e,r,n,o,i,s;for(r={},s=t.split("&"),o=0,i=s.length;i>o;o++)n=s[o],e=n.split("="),r[decodeURIComponent(e[0])]=decodeURIComponent(e[1]);return r},e.prototype.onReadyStateChange=function(){var e,r,n,o,i,s,a,h,u;if(4!==this.xhr.readyState)return!0;if(200>this.xhr.status||this.xhr.status>=300)return e=new t.ApiError(this.xhr,this.method,this.url),this.callback(e),!0;if(s=this.xhr.getResponseHeader("x-dropbox-metadata"),null!=s?s.length:void 0)try{i=JSON.parse(s)}catch(l){i=void 0}else i=void 0;if(this.responseType){if("b"===this.responseType){for(n=null!=this.xhr.responseText?this.xhr.responseText:this.xhr.response,r=[],o=h=0,u=n.length;u>=0?u>h:h>u;o=u>=0?++h:--h)r.push(String.fromCharCode(255&n.charCodeAt(o)));a=r.join(""),this.callback(null,a,i)}else this.callback(null,this.xhr.response,i);return!0}switch(a=null!=this.xhr.responseText?this.xhr.responseText:this.xhr.response,this.xhr.getResponseHeader("Content-Type")){case"application/x-www-form-urlencoded":this.callback(null,t.Xhr.urlDecode(a),i);break;case"application/json":case"text/javascript":this.callback(null,JSON.parse(a),i);break;default:this.callback(null,a,i)}return!0},e.prototype.onLoad=function(){var e;switch(e=this.xhr.responseText,this.xhr.contentType){case"application/x-www-form-urlencoded":this.callback(null,t.Xhr.urlDecode(e),void 0);break;case"application/json":case"text/javascript":this.callback(null,JSON.parse(e),void 0);break;default:this.callback(null,e,void 0)}return!0},e.prototype.onError=function(){var e;return e=new t.ApiError(this.xhr,this.method,this.url),this.callback(e),!0},e}(),null!=("undefined"!=typeof module&&null!==module?module.exports:void 0))module.exports=t;else{if("undefined"==typeof window||null===window)throw Error("This library only supports node.js and modern browsers.");window.Dropbox=t}t.atob=h,t.btoa=d,t.hmac=p,t.sha1=c,t.encodeKey=v}).call(this);
\ No newline at end of file
diff --git a/lib/client/storage/dropbox/package.json b/lib/client/storage/dropbox/package.json
new file mode 100644
index 00000000..dfa72764
--- /dev/null
+++ b/lib/client/storage/dropbox/package.json
@@ -0,0 +1,46 @@
+{
+ "name": "dropbox",
+ "version": "0.7.2",
+ "description": "Client library for the Dropbox API",
+ "keywords": ["dropbox", "filesystem", "storage"],
+ "homepage": "http://github.com/dropbox/dropbox-js",
+ "author": "Victor Costan (http://www.costan.us)",
+ "license": "MIT",
+ "contributors": [
+ "Aakanksha Sarda "
+ ],
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/dropbox/dropbox-js.git"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ },
+ "dependencies": {
+ "open": ">= 0.0.2",
+ "xmlhttprequest": ">= 1.5.0"
+ },
+ "devDependencies": {
+ "async": ">= 0.1.22",
+ "chai": ">= 1.4.0",
+ "codo": ">= 1.5.2",
+ "coffee-script": ">= 1.4.0",
+ "express": ">= 3.0.4",
+ "mocha": ">= 1.7.4",
+ "remove": ">= 0.1.5",
+ "sinon": ">= 1.5.2",
+ "sinon-chai": ">= 2.2.0",
+ "uglify-js": ">= 2.2.2"
+ },
+ "main": "lib/dropbox.js",
+ "directories": {
+ "doc": "doc",
+ "lib": "lib",
+ "src": "src",
+ "test": "test"
+ },
+ "scripts": {
+ "prepublish": "cake build",
+ "test": "cake test"
+ }
+}
diff --git a/lib/client/storage/dropbox/src/000-dropbox.coffee b/lib/client/storage/dropbox/src/000-dropbox.coffee
new file mode 100644
index 00000000..183d00c2
--- /dev/null
+++ b/lib/client/storage/dropbox/src/000-dropbox.coffee
@@ -0,0 +1,6 @@
+# Main entry point to the Dropbox API.
+class Dropbox
+ constructor: (options) ->
+ @client = new DropboxClient options
+
+ # NOTE: this is not yet implemented.
diff --git a/lib/client/storage/dropbox/src/api_error.coffee b/lib/client/storage/dropbox/src/api_error.coffee
new file mode 100644
index 00000000..131fb9ec
--- /dev/null
+++ b/lib/client/storage/dropbox/src/api_error.coffee
@@ -0,0 +1,49 @@
+# Information about a failed call to the Dropbox API.
+class Dropbox.ApiError
+ # @property {Number} the HTTP error code (e.g., 403)
+ status: undefined
+
+ # @property {String} the HTTP method of the failed request (e.g., 'GET')
+ method: undefined
+
+ # @property {String} the URL of the failed request
+ url: undefined
+
+ # @property {?String} the body of the HTTP error response; can be null if
+ # the error was caused by a network failure or by a security issue
+ responseText: undefined
+
+ # @property {?Object} the result of parsing the JSON in the HTTP error
+ # response; can be null if the API server didn't return JSON, or if the
+ # HTTP response body is unavailable
+ response: undefined
+
+ # Wraps a failed XHR call to the Dropbox API.
+ #
+ # @param {String} method the HTTP verb of the API request (e.g., 'GET')
+ # @param {String} url the URL of the API request
+ # @param {XMLHttpRequest} xhr the XMLHttpRequest instance of the failed
+ # request
+ constructor: (xhr, @method, @url) ->
+ @status = xhr.status
+ if xhr.responseType
+ text = xhr.response or xhr.responseText
+ else
+ text = xhr.responseText
+ if text
+ try
+ @responseText = text.toString()
+ @response = JSON.parse text
+ catch e
+ @response = null
+ else
+ @responseText = '(no response)'
+ @response = null
+
+ # Used when the error is printed out by developers.
+ toString: ->
+ "Dropbox API error #{@status} from #{@method} #{@url} :: #{@responseText}"
+
+ # Used by some testing frameworks.
+ inspect: ->
+ @toString()
diff --git a/lib/client/storage/dropbox/src/base64.coffee b/lib/client/storage/dropbox/src/base64.coffee
new file mode 100644
index 00000000..fa165d3a
--- /dev/null
+++ b/lib/client/storage/dropbox/src/base64.coffee
@@ -0,0 +1,72 @@
+# node.js implementation of atob and btoa
+
+if window?
+ if window.atob and window.btoa
+ atob = (string) -> window.atob string
+ btoa = (base64) -> window.btoa base64
+ else
+ # IE < 10 doesn't implement the standard atob / btoa functions.
+ base64Digits =
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
+
+ btoaNibble = (accumulator, bytes, result) ->
+ limit = 3 - bytes
+ accumulator <<= limit * 8
+ i = 3
+ while i >= limit
+ result.push base64Digits.charAt((accumulator >> (i * 6)) & 0x3F)
+ i -= 1
+ i = bytes
+ while i < 3
+ result.push '='
+ i += 1
+ null
+ atobNibble = (accumulator, digits, result) ->
+ limit = 4 - digits
+ accumulator <<= limit * 6
+ i = 2
+ while i >= limit
+ result.push String.fromCharCode((accumulator >> (8 * i)) & 0xFF)
+ i -= 1
+ null
+
+ btoa = (string) ->
+ result = []
+ accumulator = 0
+ bytes = 0
+ for i in [0...string.length]
+ accumulator = (accumulator << 8) | string.charCodeAt(i)
+ bytes += 1
+ if bytes is 3
+ btoaNibble accumulator, bytes, result
+ accumulator = bytes = 0
+
+ if bytes > 0
+ btoaNibble accumulator, bytes, result
+ result.join ''
+
+ atob = (base64) ->
+ result = []
+ accumulator = 0
+ digits = 0
+ for i in [0...base64.length]
+ digit = base64.charAt i
+ break if digit is '='
+ accumulator = (accumulator << 6) | base64Digits.indexOf(digit)
+ digits += 1
+ if digits is 4
+ atobNibble accumulator, digits, result
+ accumulator = digits = 0
+
+ if digits > 0
+ atobNibble accumulator, digits, result
+ result.join ''
+
+else
+ # NOTE: the npm packages atob and btoa don't do base64-encoding correctly.
+ atob = (arg) ->
+ buffer = new Buffer arg, 'base64'
+ (String.fromCharCode(buffer[i]) for i in [0...buffer.length]).join ''
+ btoa = (arg) ->
+ buffer = new Buffer(arg.charCodeAt(i) for i in [0...arg.length])
+ buffer.toString 'base64'
diff --git a/lib/client/storage/dropbox/src/client.coffee b/lib/client/storage/dropbox/src/client.coffee
new file mode 100644
index 00000000..c465e4bc
--- /dev/null
+++ b/lib/client/storage/dropbox/src/client.coffee
@@ -0,0 +1,1104 @@
+# Represents a user accessing the application.
+class Dropbox.Client
+ # Dropbox client representing an application.
+ #
+ # For an optimal user experience, applications should use a single client for
+ # all Dropbox interactions.
+ #
+ # @param {Object} options the application type and API key
+ # @option options {Boolean} sandbox true for applications that request
+ # sandbox access (access to a single folder exclusive to the app)
+ # @option options {String} key the Dropbox application's key; browser-side
+ # applications should use Dropbox.encodeKey to obtain an encoded
+ # key string, and pass it as the key option
+ # @option options {String} secret the Dropbox application's secret;
+ # browser-side applications should not use the secret option; instead,
+ # they should pass the result of Dropbox.encodeKey as the key option
+ # @option options {String} token if set, the user's access token
+ # @option options {String} tokenSecret if set, the secret for the user's
+ # access token
+ # @option options {String} uid if set, the user's Dropbox UID
+ # @option options {Number} authState if set, indicates that the token and
+ # tokenSecret are in an intermediate state in the authentication process;
+ # this option should never be set by hand, however it may be returned by
+ # calls to credentials()
+ constructor: (options) ->
+ @sandbox = options.sandbox or false
+ @apiServer = options.server or @defaultApiServer()
+ @authServer = options.authServer or @defaultAuthServer()
+ @fileServer = options.fileServer or @defaultFileServer()
+ @downloadServer = options.downloadServer or @defaultDownloadServer()
+
+ @oauth = new Dropbox.Oauth options
+ @driver = null
+ @filter = null
+ @uid = null
+ @authState = null
+ @authError = null
+ @_credentials = null
+ @setCredentials options
+
+ @setupUrls()
+
+ # Plugs in the OAuth / application integration code.
+ #
+ # @param {DropboxAuthDriver} driver provides the integration between the
+ # application and the Dropbox OAuth flow; most applications should be
+ # able to use instances of Dropbox.Driver.Redirect, Dropbox.Driver.Popup,
+ # or Dropbox.Driver.NodeServer
+ # @return {Dropbox.Client} this, for easy call chaining
+ authDriver: (driver) ->
+ @driver = driver
+ @
+
+ # Plugs in a filter for all XMLHttpRequests issued by this client.
+ #
+ # Whenever possible, filter implementations should only use the native
+ # XMHttpRequest object received as the first argument. The Dropbox.Xhr API
+ # implemented by the second argument is not yet stabilized.
+ #
+ # @param {function(XMLHttpRequest, Dropbox.Xhr): boolean} filter called every
+ # time the client is about to send a network request; the filter can
+ # inspect and modify the XMLHttpRequest; if the filter returns a falsey
+ # value, the XMLHttpRequest will not be sent
+ # @return {Drobpox.Client} this, for easy call chaining
+ xhrFilter: (filter) ->
+ @filter = filter
+ @
+
+ # The authenticated user's Dropbx user ID.
+ #
+ # This user ID is guaranteed to be consistent across API calls from the same
+ # application (not across applications, though).
+ #
+ # @return {?String} a short ID that identifies the user, or null if no user
+ # is authenticated
+ dropboxUid: ->
+ @uid
+
+ # Get the client's OAuth credentials.
+ #
+ # @param {?Object} the result of a prior call to credentials()
+ # @return {Object} a plain object whose properties can be passed to the
+ # Dropbox.Client constructor to reuse this client's login credentials
+ credentials: () ->
+ @computeCredentials() unless @_credentials
+ @_credentials
+
+ # Authenticates the app's user to Dropbox' API server.
+ #
+ # @param {function(?Dropbox.ApiError, ?Dropbox.Client)} callback called when
+ # the authentication completes; if successful, the second parameter is
+ # this client and the first parameter is null
+ # @return {Dropbox.Client} this, for easy call chaining
+ authenticate: (callback) ->
+ oldAuthState = null
+
+ # Advances the authentication FSM by one step.
+ _fsmStep = =>
+ if oldAuthState isnt @authState
+ oldAuthState = @authState
+ if @driver.onAuthStateChange
+ return @driver.onAuthStateChange(@, _fsmStep)
+
+ switch @authState
+ when DropboxClient.RESET # No user credentials -> request token.
+ @requestToken (error, data) =>
+ if error
+ @authError = error
+ @authState = DropboxClient.ERROR
+ else
+ token = data.oauth_token
+ tokenSecret = data.oauth_token_secret
+ @oauth.setToken token, tokenSecret
+ @authState = DropboxClient.REQUEST
+ @_credentials = null
+ _fsmStep()
+
+ when DropboxClient.REQUEST # Have request token, get it authorized.
+ authUrl = @authorizeUrl @oauth.token
+ @driver.doAuthorize authUrl, @oauth.token, @oauth.tokenSecret, =>
+ @authState = DropboxClient.AUTHORIZED
+ @_credentials = null
+ _fsmStep()
+
+ when DropboxClient.AUTHORIZED
+ # Request token authorized, switch it for an access token.
+ @getAccessToken (error, data) =>
+ if error
+ @authError = error
+ @authState = DropboxClient.ERROR
+ else
+ @oauth.setToken data.oauth_token, data.oauth_token_secret
+ @uid = data.uid
+ @authState = DropboxClient.DONE
+ @_credentials = null
+ _fsmStep()
+
+ when DropboxClient.DONE # We have an access token.
+ callback null, @
+
+ when Dropbox.SIGNED_OFF # The user signed off, restart the flow.
+ @reset()
+ _fsmStep()
+
+ when DropboxClient.ERROR # An error occurred during authentication.
+ callback @authError
+
+ _fsmStep() # Start up the state machine.
+ @
+
+ # Revokes the user's Dropbox credentials.
+ #
+ # This should be called when the user explictly signs off from your
+ # application, to meet the users' expectation that after they sign off, their
+ # access tokens will be persisted on the machine.
+ #
+ # @param {function(?Dropbox.ApiError)} callback called when
+ # the authentication completes; if successful, the error parameter is
+ # null
+ # @return {XMLHttpRequest} the XHR object used for this API call
+ signOut: (callback) ->
+ xhr = new Dropbox.Xhr 'POST', @urls.signOut
+ xhr.signWithOauth @oauth
+ @dispatchXhr xhr, (error) =>
+ return callback(error) if error
+
+ @reset()
+ @authState = DropboxClient.SIGNED_OFF
+ if @driver.onAuthStateChange
+ @driver.onAuthStateChange @, ->
+ callback error
+ else
+ callback error
+
+ # Alias for signOut.
+ signOff: (callback) ->
+ @signOut callback
+
+ # Retrieves information about the logged in user.
+ #
+ # @param {function(?Dropbox.ApiError, ?Dropbox.UserInfo, ?Object)} callback
+ # called with the result of the /account/info HTTP request; if the call
+ # succeeds, the second parameter is a Dropbox.UserInfo instance, the
+ # third parameter is the parsed JSON data behind the Dropbox.UserInfo
+ # instance, and the first parameter is null
+ # @return {XMLHttpRequest} the XHR object used for this API call
+ getUserInfo: (callback) ->
+ xhr = new Dropbox.Xhr 'GET', @urls.accountInfo
+ xhr.signWithOauth @oauth
+ @dispatchXhr xhr, (error, userData) ->
+ callback error, Dropbox.UserInfo.parse(userData), userData
+
+ # Retrieves the contents of a file stored in Dropbox.
+ #
+ # Some options are silently ignored in Internet Explorer 9 and below, due to
+ # insufficient support in its proprietary XDomainRequest replacement for XHR.
+ # Currently, the options are: arrayBuffer, blob, length, start.
+ #
+ # @param {String} path the path of the file to be read, relative to the
+ # user's Dropbox or to the application's folder
+ # @param {?Object} options the advanced settings below; for the default
+ # settings, skip the argument or pass null
+ # @option options {String} versionTag the tag string for the desired version
+ # of the file contents; the most recent version is retrieved by default
+ # @option options {String} rev alias for "versionTag" that matches the HTTP
+ # API
+ # @option options {Boolean} arrayBuffer if true, the file's contents will be
+ # passed to the callback in an ArrayBuffer; this is a good method of
+ # reading non-UTF8 data, such as images; requires XHR Level 2 support,
+ # which is not available in IE <= 9
+ # @option options {Boolean} blob if true, the file's contents will be
+ # passed to the callback in a Blob; this is a good method of reading
+ # non-UTF8 data, such as images; requires XHR Level 2 support, which is not
+ # available in IE <= 9
+ # @option options {Boolean} binary if true, the file will be retrieved as a
+ # binary string; the default is an UTF-8 encoded string; this relies on
+ # browser hacks and should not be used if the environment supports the Blob
+ # API
+ # @option options {Number} length the number of bytes to be retrieved from
+ # the file; if the start option is not present, the last "length" bytes
+ # will be read (after issue #30 is closed); by default, the entire file is
+ # read
+ # @option options {Number} start the 0-based offset of the first byte to be
+ # retrieved; if the length option is not present, the bytes between
+ # "start" and the file's end will be read; by default, the entire
+ # file is read
+ # @param {function(?Dropbox.ApiError, ?String, ?Dropbox.Stat)} callback
+ # called with the result of the /files (GET) HTTP request; the second
+ # parameter is the contents of the file, the third parameter is a
+ # Dropbox.Stat instance describing the file, and the first parameter is
+ # null
+ # @return {XMLHttpRequest} the XHR object used for this API call
+ readFile: (path, options, callback) ->
+ if (not callback) and (typeof options is 'function')
+ callback = options
+ options = null
+
+ params = {}
+ responseType = null
+ rangeHeader = null
+ if options
+ if options.versionTag
+ params.rev = options.versionTag
+ else if options.rev
+ params.rev = options.rev
+
+ if options.arrayBuffer
+ responseType = 'arraybuffer'
+ else if options.blob
+ responseType = 'blob'
+ else if options.binary
+ responseType = 'b' # See the Dropbox.Xhr.request2 docs
+
+ if options.length
+ if options.start?
+ rangeStart = options.start
+ rangeEnd = options.start + options.length - 1
+ else
+ rangeStart = ''
+ rangeEnd = options.length
+ rangeHeader = "bytes=#{rangeStart}-#{rangeEnd}"
+ else if options.start?
+ rangeHeader = "bytes=#{options.start}-"
+
+ xhr = new Dropbox.Xhr 'GET', "#{@urls.getFile}/#{@urlEncodePath(path)}"
+ xhr.setParams(params).signWithOauth(@oauth).setResponseType(responseType)
+ xhr.setHeader 'Range', rangeHeader if rangeHeader
+ @dispatchXhr xhr, (error, data, metadata) ->
+ callback error, data, Dropbox.Stat.parse(metadata)
+
+ # Store a file into a user's Dropbox.
+ #
+ # @param {String} path the path of the file to be created, relative to the
+ # user's Dropbox or to the application's folder
+ # @param {String, ArrayBuffer, ArrayBufferView, Blob, File} data the contents
+ # written to the file; if a File is passed, its name is ignored
+ # @param {?Object} options the advanced settings below; for the default
+ # settings, skip the argument or pass null
+ # @option options {String} lastVersionTag the identifier string for the
+ # version of the file's contents that was last read by this program, used
+ # for conflict resolution; for best results, use the versionTag attribute
+ # value from the Dropbox.Stat instance provided by readFile
+ # @option options {String} parentRev alias for "lastVersionTag" that matches
+ # the HTTP API
+ # @option options {Boolean} noOverwrite if set, the write will not overwrite
+ # a file with the same name that already exsits; instead the contents
+ # will be written to a similarly named file (e.g. "notes (1).txt"
+ # instead of "notes.txt")
+ # @param {function(?Dropbox.ApiError, ?Dropbox.Stat)} callback called with
+ # the result of the /files (POST) HTTP request; the second paramter is a
+ # Dropbox.Stat instance describing the newly created file, and the first
+ # parameter is null
+ # @return {XMLHttpRequest} the XHR object used for this API call
+ writeFile: (path, data, options, callback) ->
+ if (not callback) and (typeof options is 'function')
+ callback = options
+ options = null
+
+ useForm = Dropbox.Xhr.canSendForms and typeof data is 'object'
+ if useForm
+ @writeFileUsingForm path, data, options, callback
+ else
+ @writeFileUsingPut path, data, options, callback
+
+ # writeFile implementation that uses the POST /files API.
+ #
+ # @private
+ # This method is more demanding in terms of CPU and browser support, but does
+ # not require CORS preflight, so it always completes in 1 HTTP request.
+ writeFileUsingForm: (path, data, options, callback) ->
+ # Break down the path into a file/folder name and the containing folder.
+ slashIndex = path.lastIndexOf '/'
+ if slashIndex is -1
+ fileName = path
+ path = ''
+ else
+ fileName = path.substring slashIndex
+ path = path.substring 0, slashIndex
+
+ params = { file: fileName }
+ if options
+ if options.noOverwrite
+ params.overwrite = 'false'
+ if options.lastVersionTag
+ params.parent_rev = options.lastVersionTag
+ else if options.parentRev or options.parent_rev
+ params.parent_rev = options.parentRev or options.parent_rev
+ # TODO: locale support would edit the params here
+
+ xhr = new Dropbox.Xhr 'POST', "#{@urls.postFile}/#{@urlEncodePath(path)}"
+ xhr.setParams(params).signWithOauth(@oauth).setFileField('file', fileName,
+ data, 'application/octet-stream')
+
+ # NOTE: the Dropbox API docs ask us to replace the 'file' parameter after
+ # signing the request; the hack below works as intended
+ delete params.file
+
+ @dispatchXhr xhr, (error, metadata) ->
+ callback error, Dropbox.Stat.parse(metadata)
+
+ # writeFile implementation that uses the /files_put API.
+ #
+ # @private
+ # This method is less demanding on CPU, and makes fewer assumptions about
+ # browser support, but it takes 2 HTTP requests for binary files, because it
+ # needs CORS preflight.
+ writeFileUsingPut: (path, data, options, callback) ->
+ params = {}
+ if options
+ if options.noOverwrite
+ params.overwrite = 'false'
+ if options.lastVersionTag
+ params.parent_rev = options.lastVersionTag
+ else if options.parentRev or options.parent_rev
+ params.parent_rev = options.parentRev or options.parent_rev
+ # TODO: locale support would edit the params here
+ xhr = new Dropbox.Xhr 'POST', "#{@urls.putFile}/#{@urlEncodePath(path)}"
+ xhr.setBody(data).setParams(params).signWithOauth(@oauth)
+ @dispatchXhr xhr, (error, metadata) ->
+ callback error, Dropbox.Stat.parse(metadata)
+
+ # Reads the metadata of a file or folder in a user's Dropbox.
+ #
+ # @param {String} path the path to the file or folder whose metadata will be
+ # read, relative to the user's Dropbox or to the application's folder
+ # @param {?Object} options the advanced settings below; for the default
+ # settings, skip the argument or pass null
+ # @option options {Number} version if set, the call will return the metadata
+ # for the given revision of the file / folder; the latest version is used
+ # by default
+ # @option options {Boolean} removed if set to true, the results will include
+ # files and folders that were deleted from the user's Dropbox
+ # @option options {Boolean} deleted alias for "removed" that matches the HTTP
+ # API; using this alias is not recommended, because it may cause confusion
+ # with JavaScript's delete operation
+ # @option options {Boolean, Number} readDir only meaningful when stat-ing
+ # folders; if this is set, the API call will also retrieve the folder's
+ # contents, which is passed into the callback's third parameter; if this
+ # is a number, it specifies the maximum number of files and folders that
+ # should be returned; the default limit is 10,000 items; if the limit is
+ # exceeded, the call will fail with an error
+ # @option options {String} versionTag used for saving bandwidth when getting
+ # a folder's contents; if this value is specified and it matches the
+ # folder's contents, the call will fail with a 304 (Contents not changed)
+ # error code; a folder's version identifier can be obtained from the
+ # versionTag attribute of a Dropbox.Stat instance describing it
+ # @param {function(?Dropbox.ApiError, ?Dropbox.Stat, ?Array)}
+ # callback called with the result of the /metadata HTTP request; if the
+ # call succeeds, the second parameter is a Dropbox.Stat instance
+ # describing the file / folder, and the first parameter is null;
+ # if the readDir option is true and the call succeeds, the third
+ # parameter is an array of Dropbox.Stat instances describing the folder's
+ # entries
+ # @return {XMLHttpRequest} the XHR object used for this API call
+ stat: (path, options, callback) ->
+ if (not callback) and (typeof options is 'function')
+ callback = options
+ options = null
+
+ params = {}
+ if options
+ if options.version?
+ params.rev = options.version
+ if options.removed or options.deleted
+ params.include_deleted = 'true'
+ if options.readDir
+ params.list = 'true'
+ if options.readDir isnt true
+ params.file_limit = options.readDir.toString()
+ if options.cacheHash
+ params.hash = options.cacheHash
+ params.include_deleted ||= 'false'
+ params.list ||= 'false'
+ # TODO: locale support would edit the params here
+ xhr = new Dropbox.Xhr 'GET', "#{@urls.metadata}/#{@urlEncodePath(path)}"
+ xhr.setParams(params).signWithOauth @oauth
+ @dispatchXhr xhr, (error, metadata) ->
+ stat = Dropbox.Stat.parse metadata
+ if metadata?.contents
+ entries = (Dropbox.Stat.parse(entry) for entry in metadata.contents)
+ else
+ entries = undefined
+ callback error, stat, entries
+
+ # Lists the files and folders inside a folder in a user's Dropbox.
+ #
+ # @param {String} path the path to the folder whose contents will be
+ # retrieved, relative to the user's Dropbox or to the application's
+ # folder
+ # @param {?Object} options the advanced settings below; for the default
+ # settings, skip the argument or pass null
+ # @option options {Boolean} removed if set to true, the results will include
+ # files and folders that were deleted from the user's Dropbox
+ # @option options {Boolean} deleted alias for "removed" that matches the HTTP
+ # API; using this alias is not recommended, because it may cause confusion
+ # with JavaScript's delete operation
+ # @option options {Boolean, Number} limit the maximum number of files and
+ # folders that should be returned; the default limit is 10,000 items; if
+ # the limit is exceeded, the call will fail with an error
+ # @option options {String} versionTag used for saving bandwidth; if this
+ # option is specified, and its value matches the folder's version tag,
+ # the call will fail with a 304 (Contents not changed) error code
+ # instead of returning the contents; a folder's version identifier can be
+ # obtained from the versionTag attribute of a Dropbox.Stat instance
+ # describing it
+ # @param {function(?Dropbox.ApiError, ?Array, ?Dropbox.Stat,
+ # ?Array)} callback called with the result of the /metadata
+ # HTTP request; if the call succeeds, the second parameter is an array
+ # containing the names of the files and folders in the given folder, the
+ # third parameter is a Dropbox.Stat instance describing the folder, the
+ # fourth parameter is an array of Dropbox.Stat instances describing the
+ # folder's entries, and the first parameter is null
+ # @return {XMLHttpRequest} the XHR object used for this API call
+ readdir: (path, options, callback) ->
+ if (not callback) and (typeof options is 'function')
+ callback = options
+ options = null
+
+ statOptions = { readDir: true }
+ if options
+ if options.limit?
+ statOptions.readDir = options.limit
+ if options.versionTag
+ statOptions.versionTag = options.versionTag
+ @stat path, statOptions, (error, stat, entry_stats) ->
+ if entry_stats
+ entries = (entry_stat.name for entry_stat in entry_stats)
+ else
+ entries = null
+ callback error, entries, stat, entry_stats
+
+ # Alias for "stat" that matches the HTTP API.
+ metadata: (path, options, callback) ->
+ @stat path, options, callback
+
+ # Creates a publicly readable URL to a file or folder in the user's Dropbox.
+ #
+ # @param {String} path the path to the file or folder that will be linked to;
+ # the path is relative to the user's Dropbox or to the application's
+ # folder
+ # @param {?Object} options the advanced settings below; for the default
+ # settings, skip the argument or pass null
+ # @option options {Boolean} download if set, the URL will be a direct
+ # download URL, instead of the usual Dropbox preview URLs; direct
+ # download URLs are short-lived (currently 4 hours), whereas regular URLs
+ # virtually have no expiration date (currently set to 2030); no direct
+ # download URLs can be generated for directories
+ # @option options {Boolean} downloadHack if set, a long-living download URL
+ # will be generated by asking for a preview URL and using the officially
+ # documented hack at https://www.dropbox.com/help/201 to turn the preview
+ # URL into a download URL
+ # @option options {Boolean} long if set, the URL will not be shortened using
+ # Dropbox's shortner; the download and downloadHack options imply long
+ # @option options {Boolean} longUrl synonym for long; makes life easy for
+ # RhinoJS users
+ # @param {function(?Dropbox.ApiError, ?Dropbox.PublicUrl)} callback called
+ # with the result of the /shares or /media HTTP request; if the call
+ # succeeds, the second parameter is a Dropbox.PublicUrl instance, and the
+ # first parameter is null
+ # @return {XMLHttpRequest} the XHR object used for this API call
+ makeUrl: (path, options, callback) ->
+ if (not callback) and (typeof options is 'function')
+ callback = options
+ options = null
+
+ # NOTE: cannot use options.long; normally, the CoffeeScript compiler
+ # escapes keywords for us; although long isn't really a keyword, the
+ # Rhino VM thinks it is; this hack can be removed when the bug below
+ # is fixed:
+ # https://github.com/mozilla/rhino/issues/93
+ if options and (options['long'] or options.longUrl or options.downloadHack)
+ params = { short_url: 'false' }
+ else
+ params = {}
+
+ path = @urlEncodePath path
+ url = "#{@urls.shares}/#{path}"
+ isDirect = false
+ useDownloadHack = false
+ if options
+ if options.downloadHack
+ isDirect = true
+ useDownloadHack = true
+ else if options.download
+ isDirect = true
+ url = "#{@urls.media}/#{path}"
+
+ # TODO: locale support would edit the params here
+ xhr = new Dropbox.Xhr('POST', url).setParams(params).signWithOauth(@oauth)
+ @dispatchXhr xhr, (error, urlData) ->
+ if useDownloadHack and urlData and urlData.url
+ urlData.url = urlData.url.replace(@authServer, @downloadServer)
+ callback error, Dropbox.PublicUrl.parse(urlData, isDirect)
+
+ # Retrieves the revision history of a file in a user's Dropbox.
+ #
+ # @param {String} path the path to the file whose revision history will be
+ # retrieved, relative to the user's Dropbox or to the application's
+ # folder
+ # @param {?Object} options the advanced settings below; for the default
+ # settings, skip the argument or pass null
+ # @option options {Number} limit if specified, the call will return at most
+ # this many versions
+ # @param {function(?Dropbox.ApiError, ?Array)} callback called
+ # with the result of the /revisions HTTP request; if the call succeeds,
+ # the second parameter is an array with one Dropbox.Stat instance per
+ # file version, and the first parameter is null
+ # @return {XMLHttpRequest} the XHR object used for this API call
+ history: (path, options, callback) ->
+ if (not callback) and (typeof options is 'function')
+ callback = options
+ options = null
+
+ params = {}
+ if options and options.limit?
+ params.rev_limit = options.limit
+
+ xhr = new Dropbox.Xhr 'GET', "#{@urls.revisions}/#{@urlEncodePath(path)}"
+ xhr.setParams(params).signWithOauth(@oauth)
+ @dispatchXhr xhr, (error, versions) ->
+ if versions
+ stats = (Dropbox.Stat.parse(metadata) for metadata in versions)
+ else
+ stats = undefined
+ callback error, stats
+
+ # Alias for "history" that matches the HTTP API.
+ revisions: (path, options, callback) ->
+ @history path, options, callback
+
+ # Computes a URL that generates a thumbnail for a file in the user's Dropbox.
+ #
+ # @param {String} path the path to the file whose thumbnail image URL will be
+ # computed, relative to the user's Dropbox or to the application's
+ # folder
+ # @param {?Object} options the advanced settings below; for the default
+ # settings, skip the argument or pass null
+ # @option options {Boolean} png if true, the thumbnail's image will be a PNG
+ # file; the default thumbnail format is JPEG
+ # @option options {String} format value that gets passed directly to the API;
+ # this is intended for newly added formats that the API may not support;
+ # use options such as "png" when applicable
+ # @option options {String} sizeCode specifies the image's dimensions; this
+ # gets passed directly to the API; currently, the following values are
+ # supported: 'small' (32x32), 'medium' (64x64), 'large' (128x128),
+ # 's' (64x64), 'm' (128x128), 'l' (640x480), 'xl' (1024x768); the default
+ # value is "small"
+ # @return {String} a URL to an image that can be used as the thumbnail for
+ # the given file
+ thumbnailUrl: (path, options) ->
+ xhr = @thumbnailXhr path, options
+ xhr.paramsToUrl().url
+
+ # Retrieves the image data of a thumbnail for a file in the user's Dropbox.
+ #
+ # This method is intended to be used with low-level painting APIs. Whenever
+ # possible, it is easier to place the result of thumbnailUrl in a DOM
+ # element, and rely on the browser to fetch the file.
+ #
+ # @param {String} path the path to the file whose thumbnail image URL will be
+ # computed, relative to the user's Dropbox or to the application's
+ # folder
+ # @param {?Object} options the advanced settings below; for the default
+ # settings, skip the argument or pass null
+ # @option options {Boolean} png if true, the thumbnail's image will be a PNG
+ # file; the default thumbnail format is JPEG
+ # @option options {String} format value that gets passed directly to the API;
+ # this is intended for newly added formats that the API may not support;
+ # use options such as "png" when applicable
+ # @option options {String} sizeCode specifies the image's dimensions; this
+ # gets passed directly to the API; currently, the following values are
+ # supported: 'small' (32x32), 'medium' (64x64), 'large' (128x128),
+ # 's' (64x64), 'm' (128x128), 'l' (640x480), 'xl' (1024x768); the default
+ # value is "small"
+ # @option options {Boolean} blob if true, the file will be retrieved as a
+ # Blob, instead of a String; this requires XHR Level 2 support, which is
+ # not available in IE <= 9
+ # @param {function(?Dropbox.ApiError, ?Object, ?Dropbox.Stat)} callback
+ # called with the result of the /thumbnails HTTP request; if the call
+ # succeeds, the second parameter is the image data as a String or Blob,
+ # the third parameter is a Dropbox.Stat instance describing the
+ # thumbnailed file, and the first argument is null
+ # @return {XMLHttpRequest} the XHR object used for this API call
+ readThumbnail: (path, options, callback) ->
+ if (not callback) and (typeof options is 'function')
+ callback = options
+ options = null
+
+ responseType = 'b'
+ if options
+ responseType = 'blob' if options.blob
+
+ xhr = @thumbnailXhr path, options
+ xhr.setResponseType responseType
+ @dispatchXhr xhr, (error, data, metadata) ->
+ callback error, data, Dropbox.Stat.parse(metadata)
+
+ # Sets up an XHR for reading a thumbnail for a file in the user's Dropbox.
+ #
+ # @see Dropbox.Client#thumbnailUrl
+ # @return {Dropbox.Xhr} an XHR request configured for fetching the thumbnail
+ thumbnailXhr: (path, options) ->
+ params = {}
+ if options
+ if options.format
+ params.format = options.format
+ else if options.png
+ params.format = 'png'
+ if options.size
+ # Can we do something nicer here?
+ params.size = options.size
+
+ xhr = new Dropbox.Xhr 'GET', "#{@urls.thumbnails}/#{@urlEncodePath(path)}"
+ xhr.setParams(params).signWithOauth(@oauth)
+
+ # Reverts a file's contents to a previous version.
+ #
+ # This is an atomic, bandwidth-optimized equivalent of reading the file
+ # contents at the given file version (readFile), and then using it to
+ # overwrite the file (writeFile).
+ #
+ # @param {String} path the path to the file whose contents will be reverted
+ # to a previous version, relative to the user's Dropbox or to the
+ # application's folder
+ # @param {String} versionTag the tag of the version that the file will be
+ # reverted to; maps to the "rev" parameter in the HTTP API
+ # @param {function(?Dropbox.ApiError, ?Dropbox.Stat)} callback called with
+ # the result of the /restore HTTP request; if the call succeeds, the
+ # second parameter is a Dropbox.Stat instance describing the file after
+ # the revert operation, and the first parameter is null
+ # @return {XMLHttpRequest} the XHR object used for this API call
+ revertFile: (path, versionTag, callback) ->
+ xhr = new Dropbox.Xhr 'POST', "#{@urls.restore}/#{@urlEncodePath(path)}"
+ xhr.setParams(rev: versionTag).signWithOauth @oauth
+ @dispatchXhr xhr, (error, metadata) ->
+ callback error, Dropbox.Stat.parse(metadata)
+
+ # Alias for "revertFile" that matches the HTTP API.
+ restore: (path, versionTag, callback) ->
+ @revertFile path, versionTag, callback
+
+ # Finds files / folders whose name match a pattern, in the user's Dropbox.
+ #
+ # @param {String} path the path to the file whose contents will be reverted
+ # to a previous version, relative to the user's Dropbox or to the
+ # application's folder
+ # @param {String} namePattern the string that file / folder names must
+ # contain in order to match the search criteria;
+ # @param {?Object} options the advanced settings below; for the default
+ # settings, skip the argument or pass null
+ # @option options {Number} limit if specified, the call will return at most
+ # this many versions
+ # @option options {Boolean} removed if set to true, the results will include
+ # files and folders that were deleted from the user's Dropbox; the default
+ # limit is the maximum value of 1,000
+ # @option options {Boolean} deleted alias for "removed" that matches the HTTP
+ # API; using this alias is not recommended, because it may cause confusion
+ # with JavaScript's delete operation
+ # @param {function(?Dropbox.ApiError, ?Array)} callback called
+ # with the result of the /search HTTP request; if the call succeeds, the
+ # second parameter is an array with one Dropbox.Stat instance per search
+ # result, and the first parameter is null
+ # @return {XMLHttpRequest} the XHR object used for this API call
+ findByName: (path, namePattern, options, callback) ->
+ if (not callback) and (typeof options is 'function')
+ callback = options
+ options = null
+
+ params = { query: namePattern }
+ if options
+ if options.limit?
+ params.file_limit = options.limit
+ if options.removed or options.deleted
+ params.include_deleted = true
+
+ xhr = new Dropbox.Xhr 'GET', "#{@urls.search}/#{@urlEncodePath(path)}"
+ xhr.setParams(params).signWithOauth(@oauth)
+ @dispatchXhr xhr, (error, results) ->
+ if results
+ stats = (Dropbox.Stat.parse(metadata) for metadata in results)
+ else
+ stats = undefined
+ callback error, stats
+
+ # Alias for "findByName" that matches the HTTP API.
+ search: (path, namePattern, options, callback) ->
+ @findByName path, namePattern, options, callback
+
+ # Creates a reference used to copy a file to another user's Dropbox.
+ #
+ # @param {String} path the path to the file whose contents will be
+ # referenced, relative to the uesr's Dropbox or to the application's
+ # folder
+ # @param {function(?Dropbox.ApiError, ?Dropbox.CopyReference)} callback
+ # called with the result of the /copy_ref HTTP request; if the call
+ # succeeds, the second parameter is a Dropbox.CopyReference instance, and
+ # the first parameter is null
+ # @return {XMLHttpRequest} the XHR object used for this API call
+ makeCopyReference: (path, callback) ->
+ xhr = new Dropbox.Xhr 'GET', "#{@urls.copyRef}/#{@urlEncodePath(path)}"
+ xhr.signWithOauth @oauth
+ @dispatchXhr xhr, (error, refData) ->
+ callback error, Dropbox.CopyReference.parse(refData)
+
+ # Alias for "makeCopyReference" that matches the HTTP API.
+ copyRef: (path, callback) ->
+ @makeCopyReference path, callback
+
+ # Fetches a list of changes in the user's Dropbox since the last call.
+ #
+ # This method is intended to make full sync implementations easier and more
+ # performant. Each call returns a cursor that can be used in a future call
+ # to obtain all the changes that happened in the user's Dropbox (or
+ # application directory) between the two calls.
+ #
+ # @param {Dropbox.PulledChanges, String} cursorTag the result of a previous
+ # call to pullChanges, or a string containing a tag representing the
+ # Dropbox state that is used as the baseline for the change list; this
+ # should be obtained from a previous call to pullChanges, or be set to null
+ # / ommitted on the first call to pullChanges
+ # @param {function(?Dropbox.ApiError, ?Dropbox.PulledChanges)} callback
+ # called with the result of the /delta HTTP request; if the call
+ # succeeds, the second parameter is a Dropbox.PulledChanges describing
+ # the changes to the user's Dropbox since the pullChanges call that
+ # produced the given cursor, and the first parameter is null
+ # @return {XMLHttpRequest} the XHR object used for this API call
+ pullChanges: (cursor, callback) ->
+ if (not callback) and (typeof cursor is 'function')
+ callback = cursor
+ cursor = null
+
+ if cursor
+ if cursor.cursorTag
+ params = { cursor: cursor.cursorTag }
+ else
+ params = { cursor: cursor }
+ else
+ params = {}
+
+ xhr = new Dropbox.Xhr 'POST', @urls.delta
+ xhr.setParams(params).signWithOauth @oauth
+ @dispatchXhr xhr, (error, deltaInfo) ->
+ callback error, Dropbox.PulledChanges.parse(deltaInfo)
+
+ # Alias for "pullChanges" that matches the HTTP API.
+ delta: (cursor, callback) ->
+ @pullChanges cursor, callback
+
+ # Creates a folder in a user's Dropbox.
+ #
+ # @param {String} path the path of the folder that will be created, relative
+ # to the user's Dropbox or to the application's folder
+ # @param {function(?Dropbox.ApiError, ?Dropbox.Stat)} callback called with
+ # the result of the /fileops/create_folder HTTP request; if the call
+ # succeeds, the second parameter is a Dropbox.Stat instance describing
+ # the newly created folder, and the first parameter is null
+ # @return {XMLHttpRequest} the XHR object used for this API call
+ mkdir: (path, callback) ->
+ xhr = new Dropbox.Xhr 'POST', @urls.fileopsCreateFolder
+ xhr.setParams(root: @fileRoot, path: @normalizePath(path)).
+ signWithOauth(@oauth)
+ @dispatchXhr xhr, (error, metadata) ->
+ callback error, Dropbox.Stat.parse(metadata)
+
+ # Removes a file or diretory from a user's Dropbox.
+ #
+ # @param {String} path the path of the file to be read, relative to the
+ # user's Dropbox or to the application's folder
+ # @param {function(?Dropbox.ApiError, ?Dropbox.Stat)} callback called with
+ # the result of the /fileops/delete HTTP request; if the call succeeds,
+ # the second parameter is a Dropbox.Stat instance describing the removed
+ # file or folder, and the first parameter is null
+ # @return {XMLHttpRequest} the XHR object used for this API call
+ remove: (path, callback) ->
+ xhr = new Dropbox.Xhr 'POST', @urls.fileopsDelete
+ xhr.setParams(root: @fileRoot, path: @normalizePath(path)).
+ signWithOauth(@oauth)
+ @dispatchXhr xhr, (error, metadata) ->
+ callback error, Dropbox.Stat.parse(metadata)
+
+ # node.js-friendly alias for "remove".
+ unlink: (path, callback) ->
+ @remove path, callback
+
+ # Alias for "remove" that matches the HTTP API.
+ delete: (path, callback) ->
+ @remove path, callback
+
+ # Copies a file or folder in the user's Dropbox.
+ #
+ # This method's "from" parameter can be either a path or a copy reference
+ # obtained by a previous call to makeCopyRef. The method uses a crude
+ # heuristic to interpret the "from" string -- if it doesn't contain any
+ # slash (/) or dot (.) character, it is assumed to be a copy reference. The
+ # easiest way to work with it is to prepend "/" to every path passed to the
+ # method. The method will process paths that start with multiple /s
+ # correctly.
+ #
+ # @param {String, Dropbox.CopyReference} from the path of the file or folder
+ # that will be copied, or a Dropbox.CopyReference instance obtained by
+ # calling makeCopyRef or Dropbox.CopyReference.parse; if this is a path, it
+ # is relative to the user's Dropbox or to the application's folder
+ # @param {String} toPath the path that the file or folder will have after the
+ # method call; the path is relative to the user's Dropbox or to the
+ # application folder
+ # @param {function(?Dropbox.ApiError, ?Dropbox.Stat)} callback called with
+ # the result of the /fileops/copy HTTP request; if the call succeeds, the
+ # second parameter is a Dropbox.Stat instance describing the file or folder
+ # created by the copy operation, and the first parameter is null
+ # @return {XMLHttpRequest} the XHR object used for this API call
+ copy: (from, toPath, callback) ->
+ if (not callback) and (typeof options is 'function')
+ callback = options
+ options = null
+
+ params = { root: @fileRoot, to_path: @normalizePath(toPath) }
+ if from instanceof Dropbox.CopyReference
+ params.from_copy_ref = from.tag
+ else
+ params.from_path = @normalizePath from
+ # TODO: locale support would edit the params here
+
+ xhr = new Dropbox.Xhr 'POST', @urls.fileopsCopy
+ xhr.setParams(params).signWithOauth @oauth
+ @dispatchXhr xhr, (error, metadata) ->
+ callback error, Dropbox.Stat.parse(metadata)
+
+ # Moves a file or folder to a different location in a user's Dropbox.
+ #
+ # @param {String} fromPath the path of the file or folder that will be moved,
+ # relative to the user's Dropbox or to the application's folder
+ # @param {String} toPath the path that the file or folder will have after
+ # the method call; the path is relative to the user's Dropbox or to the
+ # application's folder
+ # @param {function(?Dropbox.ApiError, ?Dropbox.Stat)} callback called with
+ # the result of the /fileops/move HTTP request; if the call succeeds, the
+ # second parameter is a Dropbox.Stat instance describing the moved
+ # file or folder at its new location, and the first parameter is
+ # null
+ # @return {XMLHttpRequest} the XHR object used for this API call
+ move: (fromPath, toPath, callback) ->
+ if (not callback) and (typeof options is 'function')
+ callback = options
+ options = null
+
+ xhr = new Dropbox.Xhr 'POST', @urls.fileopsMove
+ xhr.setParams(
+ root: @fileRoot, from_path: @normalizePath(fromPath),
+ to_path: @normalizePath(toPath)).signWithOauth @oauth
+ @dispatchXhr xhr, (error, metadata) ->
+ callback error, Dropbox.Stat.parse(metadata)
+
+ # Removes all login information.
+ #
+ # @return {Dropbox.Client} this, for easy call chaining
+ reset: ->
+ @uid = null
+ @oauth.setToken null, ''
+ @authState = DropboxClient.RESET
+ @authError = null
+ @_credentials = null
+ @
+
+ # Change the client's OAuth credentials.
+ #
+ # @param {?Object} the result of a prior call to credentials()
+ # @return {Dropbox.Client} this, for easy call chaining
+ setCredentials: (credentials) ->
+ @oauth.reset credentials
+ @uid = credentials.uid or null
+ if credentials.authState
+ @authState = credentials.authState
+ else
+ if credentials.token
+ @authState = DropboxClient.DONE
+ else
+ @authState = DropboxClient.RESET
+ @authError = null
+ @_credentials = null
+ @
+
+ # @return {String} a string that uniquely identifies the Dropbox application
+ # of this client
+ appHash: ->
+ @oauth.appHash()
+
+ # Computes the URLs of all the Dropbox API calls.
+ #
+ # @private
+ # This is called by the constructor, and used by the other methods. It should
+ # not be used directly.
+ setupUrls: ->
+ @fileRoot = if @sandbox then 'sandbox' else 'dropbox'
+
+ @urls =
+ # Authentication.
+ requestToken: "#{@apiServer}/1/oauth/request_token"
+ authorize: "#{@authServer}/1/oauth/authorize"
+ accessToken: "#{@apiServer}/1/oauth/access_token"
+ signOut: "#{@apiServer}/1/unlink_access_token"
+
+ # Accounts.
+ accountInfo: "#{@apiServer}/1/account/info"
+
+ # Files and metadata.
+ getFile: "#{@fileServer}/1/files/#{@fileRoot}"
+ postFile: "#{@fileServer}/1/files/#{@fileRoot}"
+ putFile: "#{@fileServer}/1/files_put/#{@fileRoot}"
+ metadata: "#{@apiServer}/1/metadata/#{@fileRoot}"
+ delta: "#{@apiServer}/1/delta"
+ revisions: "#{@apiServer}/1/revisions/#{@fileRoot}"
+ restore: "#{@apiServer}/1/restore/#{@fileRoot}"
+ search: "#{@apiServer}/1/search/#{@fileRoot}"
+ shares: "#{@apiServer}/1/shares/#{@fileRoot}"
+ media: "#{@apiServer}/1/media/#{@fileRoot}"
+ copyRef: "#{@apiServer}/1/copy_ref/#{@fileRoot}"
+ thumbnails: "#{@fileServer}/1/thumbnails/#{@fileRoot}"
+
+ # File operations.
+ fileopsCopy: "#{@apiServer}/1/fileops/copy"
+ fileopsCreateFolder: "#{@apiServer}/1/fileops/create_folder"
+ fileopsDelete: "#{@apiServer}/1/fileops/delete"
+ fileopsMove: "#{@apiServer}/1/fileops/move"
+
+ # authState value for a client that experienced an authentication error.
+ @ERROR: 0
+
+ # authState value for a properly initialized client with no user credentials.
+ @RESET: 1
+
+ # authState value for a client with a request token that must be authorized.
+ @REQUEST: 2
+
+ # authState value for a client whose request token was authorized.
+ @AUTHORIZED: 3
+
+ # authState value for a client that has an access token.
+ @DONE: 4
+
+ # authState value for a client that voluntarily invalidated its access token.
+ @SIGNED_OFF: 5
+
+ # Normalizes a Dropobx path and encodes it for inclusion in a request URL.
+ #
+ # @private
+ # This is called internally by the other client functions, and should not be
+ # used outside the {Dropbox.Client} class.
+ urlEncodePath: (path) ->
+ Dropbox.Xhr.urlEncodeValue(@normalizePath(path)).replace /%2F/gi, '/'
+
+ # Normalizes a Dropbox path for API requests.
+ #
+ # @private
+ # This is an internal method. It is used by all the client methods that take
+ # paths as arguments.
+ #
+ # @param {String} path a path
+ normalizePath: (path) ->
+ if path.substring(0, 1) is '/'
+ i = 1
+ while path.substring(i, i + 1) is '/'
+ i += 1
+ path.substring i
+ else
+ path
+
+ # Generates an OAuth request token.
+ #
+ # @private
+ # This a low-level method called by authorize. Users should call authorize.
+ #
+ # @param {function(error, data)} callback called with the result of the
+ # /oauth/request_token HTTP request
+ requestToken: (callback) ->
+ xhr = new Dropbox.Xhr('POST', @urls.requestToken).signWithOauth(@oauth)
+ @dispatchXhr xhr, callback
+
+ # The URL for /oauth/authorize, embedding the user's token.
+ #
+ # @private
+ # This a low-level method called by authorize. Users should call authorize.
+ #
+ # @param {String} token the oauth_token obtained from an /oauth/request_token
+ # call
+ # @return {String} the URL that the user's browser should be redirected to in
+ # order to perform an /oauth/authorize request
+ authorizeUrl: (token) ->
+ params = { oauth_token: token, oauth_callback: @driver.url() }
+ "#{@urls.authorize}?" + Dropbox.Xhr.urlEncode(params)
+
+ # Exchanges an OAuth request token with an access token.
+ #
+ # @private
+ # This a low-level method called by authorize. Users should call authorize.
+ #
+ # @param {function(error, data)} callback called with the result of the
+ # /oauth/access_token HTTP request
+ getAccessToken: (callback) ->
+ xhr = new Dropbox.Xhr('POST', @urls.accessToken).signWithOauth(@oauth)
+ @dispatchXhr xhr, callback
+
+ # Prepares an XHR before it is sent to the server.
+ #
+ # @private
+ # This is a low-level method called by other client methods.
+ dispatchXhr: (xhr, callback) ->
+ xhr.setCallback callback
+ xhr.prepare()
+ nativeXhr = xhr.xhr
+ if @filter
+ return nativeXhr unless @filter(nativeXhr, xhr)
+ xhr.send()
+ nativeXhr
+
+ # @private
+ # @return {String} the URL to the default value for the "server" option
+ defaultApiServer: ->
+ 'https://api.dropbox.com'
+
+ # @private
+ # @return {String} the URL to the default value for the "authServer" option
+ defaultAuthServer: ->
+ @apiServer.replace 'api.', 'www.'
+
+ # @private
+ # @return {String} the URL to the default value for the "fileServer" option
+ defaultFileServer: ->
+ @apiServer.replace 'api.', 'api-content.'
+
+ # @private
+ # @return {String} the URL to the default value for the "downloadServer"
+ # option
+ defaultDownloadServer: ->
+ @apiServer.replace 'api.', 'dl.'
+
+ # Computes the cached value returned by credentials.
+ #
+ # @private
+ # @see Dropbox.Client#computeCredentials
+ computeCredentials: ->
+ value =
+ key: @oauth.key
+ sandbox: @sandbox
+ value.secret = @oauth.secret if @oauth.secret
+ if @oauth.token
+ value.token = @oauth.token
+ value.tokenSecret = @oauth.tokenSecret
+ value.uid = @uid if @uid
+ if @authState isnt DropboxClient.ERROR and
+ @authState isnt DropboxClient.RESET and
+ @authState isnt DropboxClient.DONE and
+ @authState isnt DropboxClient.SIGNED_OFF
+ value.authState = @authState
+ if @apiServer isnt @defaultApiServer()
+ value.server = @apiServer
+ if @authServer isnt @defaultAuthServer()
+ value.authServer = @authServer
+ if @fileServer isnt @defaultFileServer()
+ value.fileServer = @fileServer
+ if @downloadServer isnt @defaultDownloadServer()
+ value.downloadServer = @downloadServer
+ @_credentials = value
+
+DropboxClient = Dropbox.Client
diff --git a/lib/client/storage/dropbox/src/drivers.coffee b/lib/client/storage/dropbox/src/drivers.coffee
new file mode 100644
index 00000000..d48184e4
--- /dev/null
+++ b/lib/client/storage/dropbox/src/drivers.coffee
@@ -0,0 +1,477 @@
+# Documentation for the interface to a Dropbox OAuth driver.
+class Dropbox.AuthDriver
+ # The callback URL that should be supplied to the OAuth /authorize call.
+ #
+ # The driver must be able to intercept redirects to the returned URL, in
+ # order to know when a user has completed the authorization flow.
+ #
+ # @return {String} an absolute URL
+ url: ->
+ 'https://some.url'
+
+ # Redirects users to /authorize and waits for them to complete the flow.
+ #
+ # This method is called when the OAuth process reaches the REQUEST state,
+ # meaning the client has a request token that must be authorized by the user.
+ #
+ # @param {String} authUrl the URL that users should be sent to in order to
+ # authorize the application's token; this points to a Web page on
+ # Dropbox' servers
+ # @param {String} token the OAuth token that the user is authorizing; this
+ # will be provided by the Dropbox servers as a query parameter when the
+ # user is redirected to the URL returned by the driver's url() method
+ # @param {String} tokenSecret the secret associated with the given OAuth
+ # token; the driver may store this together with the token
+ # @param {function()} callback called when users have completed the
+ # authorization flow; the driver should call this when Dropbox redirects
+ # users to the URL returned by the url() method, and the 'token' query
+ # parameter matches the value of the token parameter
+ doAuthorize: (authUrl, token, tokenSecret, callback) ->
+ callback 'oauth-token'
+
+ # Called when there is some progress in the OAuth process.
+ #
+ # The OAuth process goes through the following states:
+ #
+ # * Dropbox.Client.RESET - the client has no OAuth token, and is about to
+ # ask for a request token
+ # * Dropbox.Client.REQUEST - the client has a request OAuth token, and the
+ # user must go to an URL on the Dropbox servers to authorize the token
+ # * Dropbox.Client.AUTHORIZED - the client has a request OAuth token that
+ # was authorized by the user, and is about to exchange it for an access
+ # token
+ # * Dropbox.Client.DONE - the client has an access OAuth token that can be
+ # used for all API calls; the OAuth process is complete, and the callback
+ # passed to authorize is about to be called
+ # * Dropbox.Client.SIGNED_OFF - the client's Dropbox.Client#signOut() was
+ # called, and the client's OAuth token was invalidated
+ # * Dropbox.Client.ERROR - the client encounered an error during the OAuth
+ # process; the callback passed to authorize is about to be called with the
+ # error information
+ #
+ # @param {Dropbox.Client} client the client performing the OAuth process
+ # @param {function()} callback called when onAuthStateChange acknowledges the
+ # state change
+ onAuthStateChange: (client, callback) ->
+ callback()
+
+# Namespace for authentication drivers.
+Dropbox.Drivers = {}
+
+# Base class for drivers that run in the browser.
+#
+# Inheriting from this class makes a driver use HTML5 localStorage to preserve
+# OAuth tokens across page reloads.
+class Dropbox.Drivers.BrowserBase
+ # Sets up the OAuth driver.
+ #
+ # Subclasses should pass the options object they receive to the superclass
+ # constructor.
+ #
+ # @param {?Object} options the advanced settings below
+ # @option options {Boolean} rememberUser if true, the user's OAuth tokens are
+ # saved in localStorage; if you use this, you MUST provide a UI item that
+ # calls signOut() on Dropbox.Client, to let the user "log out" of the
+ # application
+ # @option options {String} scope embedded in the localStorage key that holds
+ # the authentication data; useful for having multiple OAuth tokens in a
+ # single application
+ constructor: (options) ->
+ @rememberUser = options?.rememberUser or false
+ @scope = options?.scope or 'default'
+
+ # The magic happens here.
+ onAuthStateChange: (client, callback) ->
+ @setStorageKey client
+
+ switch client.authState
+ when DropboxClient.RESET
+ @loadCredentials (credentials) =>
+ return callback() unless credentials
+
+ if credentials.authState # Incomplete authentication.
+ client.setCredentials credentials
+ return callback()
+
+ # There is an old access token. Only use it if the app supports
+ # logout.
+ unless @rememberUser
+ @forgetCredentials()
+ return callback()
+
+ # Verify that the old access token still works.
+ client.setCredentials credentials
+ client.getUserInfo (error) =>
+ if error
+ client.reset()
+ @forgetCredentials callback
+ else
+ callback()
+ when DropboxClient.REQUEST
+ @storeCredentials client.credentials(), callback
+ when DropboxClient.DONE
+ if @rememberUser
+ return @storeCredentials(client.credentials(), callback)
+ @forgetCredentials callback
+ when DropboxClient.SIGNED_OFF
+ @forgetCredentials callback
+ when DropboxClient.ERROR
+ @forgetCredentials callback
+ else
+ callback()
+ @
+
+ # Computes the @storageKey used by loadCredentials and forgetCredentials.
+ #
+ # @private
+ # This is called by onAuthStateChange.
+ #
+ # @param {Dropbox.Client} client the client instance that is running the
+ # authorization process
+ # @return {Dropbox.Driver} this, for easy call chaining
+ setStorageKey: (client) ->
+ # NOTE: the storage key is dependent on the app hash so that multiple apps
+ # hosted off the same server don't step on eachother's toes
+ @storageKey = "dropbox-auth:#{@scope}:#{client.appHash()}"
+ @
+
+ # Stores a Dropbox.Client's credentials to localStorage.
+ #
+ # @private
+ # onAuthStateChange calls this method during the authentication flow.
+ #
+ # @param {Object} credentials the result of a Drobpox.Client#credentials call
+ # @param {function()} callback called when the storing operation is complete
+ # @return {Dropbox.Drivers.BrowserBase} this, for easy call chaining
+ storeCredentials: (credentials, callback) ->
+ localStorage.setItem @storageKey, JSON.stringify(credentials)
+ callback()
+ @
+
+ # Retrieves a token and secret from localStorage.
+ #
+ # @private
+ # onAuthStateChange calls this method during the authentication flow.
+ #
+ # @param {function(?Object)} callback supplied with the credentials object
+ # stored by a previous call to
+ # Dropbox.Drivers.BrowserBase#storeCredentials; null if no credentials were
+ # stored, or if the previously stored credentials were deleted
+ # @return {Dropbox.Drivers.BrowserBase} this, for easy call chaining
+ loadCredentials: (callback) ->
+ jsonString = localStorage.getItem @storageKey
+ unless jsonString
+ callback null
+ return @
+
+ try
+ callback JSON.parse(jsonString)
+ catch e
+ # Parse errors.
+ callback null
+ @
+
+ # Deletes information previously stored by a call to storeToken.
+ #
+ # @private
+ # onAuthStateChange calls this method during the authentication flow.
+ #
+ # @param {function()} callback called after the credentials are deleted
+ # @return {Dropbox.Drivers.BrowserBase} this, for easy call chaining
+ forgetCredentials: (callback) ->
+ localStorage.removeItem @storageKey
+ callback()
+ @
+
+ # Wrapper for window.location, for testing purposes.
+ #
+ # @return {String} the current page's URL
+ @currentLocation: ->
+ window.location.href
+
+# OAuth driver that uses a redirect and localStorage to complete the flow.
+class Dropbox.Drivers.Redirect extends Dropbox.Drivers.BrowserBase
+ # Sets up the redirect-based OAuth driver.
+ #
+ # @param {?Object} options the advanced settings below
+ # @option options {Boolean} useQuery if true, the page will receive OAuth
+ # data as query parameters; by default, the page receives OAuth data in
+ # the fragment part of the URL (the string following the #,
+ # available as document.location.hash), to avoid confusing the server
+ # generating the page
+ # @option options {Boolean} rememberUser if true, the user's OAuth tokens are
+ # saved in localStorage; if you use this, you MUST provide a UI item that
+ # calls signOut() on Dropbox.Client, to let the user "log out" of the
+ # application
+ # @option options {String} scope embedded in the localStorage key that holds
+ # the authentication data; useful for having multiple OAuth tokens in a
+ # single application
+ constructor: (options) ->
+ super options
+ @useQuery = options?.useQuery or false
+ @receiverUrl = @computeUrl options
+ @tokenRe = new RegExp "(#|\\?|&)oauth_token=([^]+)(&|#|$)"
+
+ # Forwards the authentication process from REQUEST to AUTHORIZED on redirect.
+ onAuthStateChange: (client, callback) ->
+ superCall = do => => super client, callback
+ @setStorageKey client
+ if client.authState is DropboxClient.RESET
+ @loadCredentials (credentials) =>
+ if credentials and credentials.authState # Incomplete authentication.
+ if credentials.token is @locationToken() and
+ credentials.authState is DropboxClient.REQUEST
+ # locationToken matched, so the redirect happened
+ credentials.authState = DropboxClient.AUTHORIZED
+ return @storeCredentials credentials, superCall
+ else
+ # The authentication process broke down, start over.
+ return @forgetCredentials superCall
+ superCall()
+ else
+ superCall()
+
+ # URL of the current page, since the user will be sent right back.
+ url: ->
+ @receiverUrl
+
+ # Redirects to the authorize page.
+ doAuthorize: (authUrl) ->
+ window.location.assign authUrl
+
+ # Pre-computes the return value of url.
+ computeUrl: ->
+ querySuffix = "_dropboxjs_scope=#{encodeURIComponent @scope}"
+ location = Dropbox.Drivers.BrowserBase.currentLocation()
+ if location.indexOf('#') is -1
+ fragment = null
+ else
+ locationPair = location.split '#', 2
+ location = locationPair[0]
+ fragment = locationPair[1]
+ if @useQuery
+ if location.indexOf('?') is -1
+ location += "?#{querySuffix}" # No query string in the URL.
+ else
+ location += "{querySuffix}" # The URL already has a query string.
+ else
+ fragment = "?#{querySuffix}"
+
+ if fragment
+ location + '#' + fragment
+ else
+ location
+
+ # Figures out if the user completed the OAuth flow based on the current URL.
+ #
+ # @return {?String} the OAuth token that the user just authorized, or null if
+ # the user accessed this directly, without having authorized a token
+ locationToken: ->
+ location = Dropbox.Drivers.BrowserBase.currentLocation()
+
+ # Check for the scope.
+ scopePattern = "_dropboxjs_scope=#{encodeURIComponent @scope}&"
+ return null if location.indexOf?(scopePattern) is -1
+
+ # Extract the token.
+ match = @tokenRe.exec location
+ if match then decodeURIComponent(match[2]) else null
+
+# OAuth driver that uses a popup window and postMessage to complete the flow.
+class Dropbox.Drivers.Popup extends Dropbox.Drivers.BrowserBase
+ # Sets up a popup-based OAuth driver.
+ #
+ # @param {?Object} options one of the settings below; leave out the argument
+ # to use the current location for redirecting
+ # @option options {Boolean} rememberUser if true, the user's OAuth tokens are
+ # saved in localStorage; if you use this, you MUST provide a UI item that
+ # calls signOut() on Dropbox.Client, to let the user "log out" of the
+ # application
+ # @option options {String} scope embedded in the localStorage key that holds
+ # the authentication data; useful for having multiple OAuth tokens in a
+ # single application
+ # @option options {String} receiverUrl URL to the page that receives the
+ # /authorize redirect and performs the postMessage
+ # @option options {Boolean} noFragment if true, the receiverUrl will be used
+ # as given; by default, a hash "#" is appended to URLs that don't have
+ # one, so the OAuth token is received as a URL fragment and does not hit
+ # the file server
+ # @option options {String} receiverFile the URL to the receiver page will be
+ # computed by replacing the file name (everything after the last /) of
+ # the current location with this parameter's value
+ constructor: (options) ->
+ super options
+ @receiverUrl = @computeUrl options
+ @tokenRe = new RegExp "(#|\\?|&)oauth_token=([^]+)(&|#|$)"
+
+ # Removes credentials stuck in the REQUEST stage.
+ onAuthStateChange: (client, callback) ->
+ superCall = do => => super client, callback
+ @setStorageKey client
+ if client.authState is DropboxClient.RESET
+ @loadCredentials (credentials) ->
+ if credentials and credentials.authState # Incomplete authentication.
+ # The authentication process broke down, start over.
+ return @forgetCredentials superCall
+ superCall()
+ else
+ superCall()
+
+ # Shows the authorization URL in a pop-up, waits for it to send a message.
+ doAuthorize: (authUrl, token, tokenSecret, callback) ->
+ @listenForMessage token, callback
+ @openWindow authUrl
+
+ # URL of the redirect receiver page, which posts a message back to this page.
+ url: ->
+ @receiverUrl
+
+ # Pre-computes the return value of url.
+ computeUrl: (options) ->
+ if options
+ if options.receiverUrl
+ if options.noFragment or options.receiverUrl.indexOf('#') isnt -1
+ return options.receiverUrl
+ else
+ return options.receiverUrl + '#'
+ else if options.receiverFile
+ fragments = Dropbox.Drivers.BrowserBase.currentLocation().split '/'
+ fragments[fragments.length - 1] = options.receiverFile
+ if options.noFragment
+ return fragments.join('/')
+ else
+ return fragments.join('/') + '#'
+ Dropbox.Drivers.BrowserBase.currentLocation()
+
+ # Creates a popup window.
+ #
+ # @param {String} url the URL that will be loaded in the popup window
+ # @return {?DOMRef} reference to the opened window, or null if the call
+ # failed
+ openWindow: (url) ->
+ window.open url, '_dropboxOauthSigninWindow', @popupWindowSpec(980, 700)
+
+ # Spec string for window.open to create a nice popup.
+ #
+ # @param {Number} popupWidth the desired width of the popup window
+ # @param {Number} popupHeight the desired height of the popup window
+ # @return {String} spec string for the popup window
+ popupWindowSpec: (popupWidth, popupHeight) ->
+ # Metrics for the current browser window.
+ x0 = window.screenX ? window.screenLeft
+ y0 = window.screenY ? window.screenTop
+ width = window.outerWidth ? document.documentElement.clientWidth
+ height = window.outerHeight ? document.documentElement.clientHeight
+
+ # Computed popup window metrics.
+ popupLeft = Math.round x0 + (width - popupWidth) / 2
+ popupTop = Math.round y0 + (height - popupHeight) / 2.5
+ popupLeft = x0 if popupLeft < x0
+ popupTop = y0 if popupTop < y0
+
+ # The specification string.
+ "width=#{popupWidth},height=#{popupHeight}," +
+ "left=#{popupLeft},top=#{popupTop}" +
+ 'dialog=yes,dependent=yes,scrollbars=yes,location=yes'
+
+ # Listens for a postMessage from a previously opened popup window.
+ #
+ # @param {String} token the token string that must be received from the popup
+ # window
+ # @param {function()} called when the received message matches the token
+ listenForMessage: (token, callback) ->
+ listener = (event) =>
+ match = @tokenRe.exec event.data.toString()
+ if match and decodeURIComponent(match[2]) is token
+ window.removeEventListener 'message', listener
+ callback()
+ window.addEventListener 'message', listener, false
+
+
+# OAuth driver that redirects the browser to a node app to complete the flow.
+#
+# This is useful for testing node.js libraries and applications.
+class Dropbox.Drivers.NodeServer
+ # Starts up the node app that intercepts the browser redirect.
+ #
+ # @param {?Object} options one or more of the options below
+ # @option options {Number} port the number of the TCP port that will receive
+ # HTTP requests
+ # @param {String} faviconFile the path to a file that will be served at
+ # /favicon.ico
+ constructor: (options) ->
+ @port = options?.port or 8912
+ @faviconFile = options?.favicon or null
+ # Calling require in the constructor because this doesn't work in browsers.
+ @fs = require 'fs'
+ @http = require 'http'
+ @open = require 'open'
+
+ @callbacks = {}
+ @urlRe = new RegExp "^/oauth_callback\\?"
+ @tokenRe = new RegExp "(\\?|&)oauth_token=([^&]+)(&|$)"
+ @createApp()
+
+ # URL to the node.js OAuth callback handler.
+ url: ->
+ "http://localhost:#{@port}/oauth_callback"
+
+ # Opens the token
+ doAuthorize: (authUrl, token, tokenSecret, callback) ->
+ @callbacks[token] = callback
+ @openBrowser authUrl
+
+ # Opens the given URL in a browser.
+ openBrowser: (url) ->
+ unless url.match /^https?:\/\//
+ throw new Error("Not a http/https URL: #{url}")
+ @open url
+
+ # Creates and starts up an HTTP server that will intercept the redirect.
+ createApp: ->
+ @app = @http.createServer (request, response) =>
+ @doRequest request, response
+ @app.listen @port
+
+ # Shuts down the HTTP server.
+ #
+ # The driver will become unusable after this call.
+ closeServer: ->
+ @app.close()
+
+ # Reads out an /authorize callback.
+ doRequest: (request, response) ->
+ if @urlRe.exec request.url
+ match = @tokenRe.exec request.url
+ if match
+ token = decodeURIComponent match[2]
+ if @callbacks[token]
+ @callbacks[token]()
+ delete @callbacks[token]
+ data = ''
+ request.on 'data', (dataFragment) -> data += dataFragment
+ request.on 'end', =>
+ if @faviconFile and (request.url is '/favicon.ico')
+ @sendFavicon response
+ else
+ @closeBrowser response
+
+ # Renders a response that will close the browser window used for OAuth.
+ closeBrowser: (response) ->
+ closeHtml = """
+
+
+
Please close this window.
+ """
+ response.writeHead(200,
+ {'Content-Length': closeHtml.length, 'Content-Type': 'text/html' })
+ response.write closeHtml
+ response.end
+
+ # Renders the favicon file.
+ sendFavicon: (response) ->
+ @fs.readFile @faviconFile, (error, data) ->
+ response.writeHead(200,
+ { 'Content-Length': data.length, 'Content-Type': 'image/x-icon' })
+ response.write data
+ response.end
diff --git a/lib/client/storage/dropbox/src/hmac.coffee b/lib/client/storage/dropbox/src/hmac.coffee
new file mode 100644
index 00000000..b26be463
--- /dev/null
+++ b/lib/client/storage/dropbox/src/hmac.coffee
@@ -0,0 +1,180 @@
+# HMAC-SHA1 implementation heavily inspired from
+# http://pajhome.org.uk/crypt/md5/sha1.js
+
+# Base64-encoded HMAC-SHA1.
+#
+# @param {String} string the ASCII string to be signed
+# @param {String} key the HMAC key
+# @return {String} a base64-encoded HMAC of the given string and key
+base64HmacSha1 = (string, key) ->
+ arrayToBase64 hmacSha1(stringToArray(string), stringToArray(key),
+ string.length, key.length)
+
+# Base64-encoded SHA1.
+#
+# @param {String} string the ASCII string to be hashed
+# @return {String} a base64-encoded SHA1 hash of the given string
+base64Sha1 = (string) ->
+ arrayToBase64 sha1(stringToArray(string), string.length)
+
+# SHA1 and HMAC-SHA1 versions that use the node.js builtin crypto.
+unless window?
+ crypto = require 'crypto'
+ base64HmacSha1 = (string, key) ->
+ hmac = crypto.createHmac 'sha1', key
+ hmac.update string
+ hmac.digest 'base64'
+ base64Sha1 = (string) ->
+ hash = crypto.createHash 'sha1'
+ hash.update string
+ hash.digest 'base64'
+
+# HMAC-SHA1 implementation.
+#
+# @param {Array} string the HMAC input, as an array of 32-bit numbers
+# @param {Array} key the HMAC input, as an array of 32-bit numbers
+# @param {Number} length the length of the HMAC input, in bytes
+# @return {Array} the HMAC output, as an array of 32-bit numbers
+hmacSha1 = (string, key, length, keyLength) ->
+ if key.length > 16
+ key = sha1 key, keyLength
+
+ ipad = (key[i] ^ 0x36363636 for i in [0...16])
+ opad = (key[i] ^ 0x5C5C5C5C for i in [0...16])
+
+ hash1 = sha1 ipad.concat(string), 64 + length
+ sha1 opad.concat(hash1), 64 + 20
+
+# SHA1 implementation.
+#
+# @param {Array} string the SHA1 input, as an array of 32-bit numbers; the
+# computation trashes the array
+# @param {Number} length the number of bytes in the SHA1 input; used in the
+# SHA1 padding algorithm
+# @return {Array} the SHA1 output, as an array of 32-bit numbers
+sha1 = (string, length) ->
+ string[length >> 2] |= 1 << (31 - ((length & 0x03) << 3))
+ string[(((length + 8) >> 6) << 4) + 15] = length << 3
+
+ state = Array 80
+ a = 1732584193 # 0x67452301
+ b = -271733879 # 0xefcdab89
+ c = -1732584194 # 0x98badcfe
+ d = 271733878 # 0x10325476
+ e = -1009589776 # 0xc3d2e1f0
+
+ i = 0
+ limit = string.length
+ # Uncomment the line below to debug packing.
+ # console.log string.map(xxx)
+ while i < limit
+ a0 = a
+ b0 = b
+ c0 = c
+ d0 = d
+ e0 = e
+
+ for j in [0...80]
+ if j < 16
+ state[j] = string[i + j]
+ else
+ state[j] = rotateLeft32 state[j - 3] ^ state[j - 8] ^ state[j - 14] ^
+ state[j - 16], 1
+ if j < 20
+ ft = (b & c) | ((~b) & d)
+ kt = 1518500249 # 0x5a827999
+ else if j < 40
+ ft = b ^ c ^ d
+ kt = 1859775393 # 0x6ed9eba1
+ else if j < 60
+ ft = (b & c) | (b & d) | (c & d)
+ kt = -1894007588 # 0x8f1bbcdc
+ else
+ ft = b ^ c ^ d
+ kt = -899497514 # 0xca62c1d6
+ t = add32 add32(rotateLeft32(a, 5), ft), add32(add32(e, state[j]), kt)
+ e = d
+ d = c
+ c = rotateLeft32 b, 30
+ b = a
+ a = t
+ # Uncomment the line below to debug block computation.
+ # console.log [xxx(a), xxx(b), xxx(c), xxx(d), xxx(e)]
+ a = add32 a, a0
+ b = add32 b, b0
+ c = add32 c, c0
+ d = add32 d, d0
+ e = add32 e, e0
+ i += 16
+ # Uncomment the line below to see the input to the base64 encoder.
+ # console.log [xxx(a), xxx(b), xxx(c), xxx(d), xxx(e)]
+ [a, b, c, d, e]
+
+###
+# Uncomment the definition below for debugging.
+#
+# Returns the hexadecimal representation of a 32-bit number.
+xxx = (n) ->
+ if n < 0
+ n = (1 << 30) * 4 + n
+ n.toString 16
+###
+
+# Rotates a 32-bit word.
+#
+# @param {Number} value the 32-bit number to be rotated
+# @param {Number} count the number of bits (0..31) to rotate by
+# @return {Number} the rotated value
+rotateLeft32 = (value, count) ->
+ (value << count) | (value >>> (32 - count))
+
+# 32-bit unsigned addition.
+#
+# @param {Number} a, b the 32-bit numbers to be added modulo 2^32
+# @return {Number} the 32-bit representation of a + b
+add32 = (a, b) ->
+ low = (a & 0xFFFF) + (b & 0xFFFF)
+ high = (a >> 16) + (b >> 16) + (low >> 16)
+ (high << 16) | (low & 0xFFFF)
+
+# Converts a 32-bit number array into a base64-encoded string.
+#
+# @param {Array} an array of big-endian 32-bit numbers
+# @return {String} base64 encoding of the given array of numbers
+arrayToBase64 = (array) ->
+ string = ""
+ i = 0
+ limit = array.length * 4
+ while i < limit
+ i2 = i
+ trit = ((array[i2 >> 2] >> ((3 - (i2 & 3)) << 3)) & 0xFF) << 16
+ i2 += 1
+ trit |= ((array[i2 >> 2] >> ((3 - (i2 & 3)) << 3)) & 0xFF) << 8
+ i2 += 1
+ trit |= (array[i2 >> 2] >> ((3 - (i2 & 3)) << 3)) & 0xFF
+
+ string += _base64Digits[(trit >> 18) & 0x3F]
+ string += _base64Digits[(trit >> 12) & 0x3F]
+ i += 1
+ if i >= limit
+ string += '='
+ else
+ string += _base64Digits[(trit >> 6) & 0x3F]
+ i += 1
+ if i >= limit
+ string += '='
+ else
+ string += _base64Digits[trit & 0x3F]
+ i += 1
+ string
+
+_base64Digits = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+# Converts an ASCII string into array of 32-bit numbers.
+stringToArray = (string) ->
+ array = []
+ mask = 0xFF
+ for i in [0...string.length]
+ array[i >> 2] |= (string.charCodeAt(i) & mask) << ((3 - (i & 3)) << 3)
+ array
+
diff --git a/lib/client/storage/dropbox/src/oauth.coffee b/lib/client/storage/dropbox/src/oauth.coffee
new file mode 100644
index 00000000..823f90a5
--- /dev/null
+++ b/lib/client/storage/dropbox/src/oauth.coffee
@@ -0,0 +1,162 @@
+# Stripped-down OAuth implementation that works with the Dropbox API server.
+class Dropbox.Oauth
+ # Creates an Oauth instance that manages an application's keys and token.
+ #
+ # @param {Object} options the following properties
+ # @option options {String} key the Dropbox application's key (consumer key,
+ # in OAuth vocabulary); browser-side applications should use
+ # Dropbox.encodeKey to obtain an encoded key string, and pass it as the
+ # key option
+ # @option options {String} secret the Dropbox application's secret (consumer
+ # secret, in OAuth vocabulary); browser-side applications should not use
+ # the secret option; instead, they should pass the result of
+ # Dropbox.encodeKey as the key option
+ constructor: (options) ->
+ @key = @k = null
+ @secret = @s = null
+ @token = null
+ @tokenSecret = null
+ @_appHash = null
+ @reset options
+
+ # Creates an Oauth instance that manages an application's keys and token.
+ #
+ # @see Dropbox.Oauth#constructor for options
+ reset: (options) ->
+ if options.secret
+ @k = @key = options.key
+ @s = @secret = options.secret
+ @_appHash = null
+ else if options.key
+ @key = options.key
+ @secret = null
+ secret = atob dropboxEncodeKey(@key).split('|', 2)[1]
+ [k, s] = secret.split '?', 2
+ @k = decodeURIComponent k
+ @s = decodeURIComponent s
+ @_appHash = null
+ else
+ unless @k
+ throw new Error('No API key supplied')
+
+ if options.token
+ @setToken options.token, options.tokenSecret
+ else
+ @setToken null, ''
+
+ # Sets the OAuth token to be used for future requests.
+ setToken: (token, tokenSecret) ->
+ if token and (not tokenSecret)
+ throw new Error('No secret supplied with the user token')
+
+ @token = token
+ @tokenSecret = tokenSecret || ''
+
+ # This is part of signing, but it's set here so it can be cached.
+ @hmacKey = Dropbox.Xhr.urlEncodeValue(@s) + '&' +
+ Dropbox.Xhr.urlEncodeValue(tokenSecret)
+ null
+
+ # Computes the value of the Authorization HTTP header.
+ #
+ # This method mutates the params object, and removes all the OAuth-related
+ # parameters from it.
+ #
+ # @param {String} method the HTTP method used to make the request ('GET',
+ # 'POST', etc)
+ # @param {String} url the HTTP URL (e.g. "http://www.example.com/photos")
+ # that receives the request
+ # @param {Object} params an associative array (hash) containing the HTTP
+ # request parameters; the parameters should include the oauth_
+ # parameters generated by calling {Dropbox.Oauth#boilerplateParams}
+ # @return {String} the value to be used for the Authorization HTTP header
+ authHeader: (method, url, params) ->
+ @addAuthParams method, url, params
+
+ # Collect all the OAuth parameters.
+ oauth_params = []
+ for param, value of params
+ if param.substring(0, 6) == 'oauth_'
+ oauth_params.push param
+ oauth_params.sort()
+
+ # Remove the parameters from the params hash and add them to the header.
+ header = []
+ for param in oauth_params
+ header.push Dropbox.Xhr.urlEncodeValue(param) + '="' +
+ Dropbox.Xhr.urlEncodeValue(params[param]) + '"'
+ delete params[param]
+
+ # NOTE: the space after the comma is optional in the OAuth spec, so we'll
+ # skip it to save some bandwidth
+ 'OAuth ' + header.join(',')
+
+ # Generates OAuth-required HTTP parameters.
+ #
+ # This method mutates the params object, and adds the OAuth-related
+ # parameters to it.
+ #
+ # @param {String} method the HTTP method used to make the request ('GET',
+ # 'POST', etc)
+ # @param {String} url the HTTP URL (e.g. "http://www.example.com/photos")
+ # that receives the request
+ # @param {Object} params an associative array (hash) containing the HTTP
+ # request parameters; the parameters should include the oauth_
+ # parameters generated by calling {Dropbox.Oauth#boilerplateParams}
+ # @return {String} the value to be used for the Authorization HTTP header
+ addAuthParams: (method, url, params) ->
+ # Augment params with OAuth parameters.
+ @boilerplateParams params
+ params.oauth_signature = @signature method, url, params
+ params
+
+ # Adds boilerplate OAuth parameters to a request's parameter list.
+ #
+ # This should be called right before signing a request, to maximize the
+ # chances that the OAuth timestamp will be fresh.
+ #
+ # @param {Object} params an associative array (hash) containing the
+ # parameters for an OAuth request; the boilerplate parameters will be
+ # added to this hash
+ # @return {Object} params
+ boilerplateParams: (params) ->
+ params.oauth_consumer_key = @k
+ params.oauth_nonce = @nonce()
+ params.oauth_signature_method = 'HMAC-SHA1'
+ params.oauth_token = @token if @token
+ params.oauth_timestamp = Math.floor(Date.now() / 1000)
+ params.oauth_version = '1.0'
+ params
+
+ # Generates a nonce for an OAuth request.
+ #
+ # @return {String} the nonce to be used as the oauth_nonce parameter
+ nonce: ->
+ Date.now().toString(36) + Math.random().toString(36)
+
+ # Computes the signature for an OAuth request.
+ #
+ # @param {String} method the HTTP method used to make the request ('GET',
+ # 'POST', etc)
+ # @param {String} url the HTTP URL (e.g. "http://www.example.com/photos")
+ # that receives the request
+ # @param {Object} params an associative array (hash) containing the HTTP
+ # request parameters; the parameters should include the oauth_
+ # parameters generated by calling {Dropbox.Oauth#boilerplateParams}
+ # @return {String} the signature, ready to be used as the oauth_signature
+ # OAuth parameter
+ signature: (method, url, params) ->
+ string = method.toUpperCase() + '&' + Dropbox.Xhr.urlEncodeValue(url) +
+ '&' + Dropbox.Xhr.urlEncodeValue(Dropbox.Xhr.urlEncode(params))
+ base64HmacSha1 string, @hmacKey
+
+ # @return {String} a string that uniquely identifies the OAuth application
+ appHash: ->
+ return @_appHash if @_appHash
+ @_appHash = base64Sha1(@k).replace(/\=/g, '')
+
+
+# Polyfill for Internet Explorer 8.
+unless Date.now?
+ Date.now = () ->
+ (new Date()).getTime()
diff --git a/lib/client/storage/dropbox/src/prod.coffee b/lib/client/storage/dropbox/src/prod.coffee
new file mode 100644
index 00000000..3cc177b9
--- /dev/null
+++ b/lib/client/storage/dropbox/src/prod.coffee
@@ -0,0 +1,36 @@
+# Necessary bits to get a browser-side app in production.
+
+# Packs up a key and secret into a string, to bring script kiddies some pain.
+#
+# @param {String} key the application's API key
+# @param {String} secret the application's API secret
+# @return {String} encoded key string that can be passed as the key option to
+# the Dropbox.Client constructor
+dropboxEncodeKey = (key, secret) ->
+ if secret
+ secret = [encodeURIComponent(key), encodeURIComponent(secret)].join('?')
+ key = for i in [0...(key.length / 2)]
+ ((key.charCodeAt(i * 2) & 15) * 16) + (key.charCodeAt(i * 2 + 1) & 15)
+ else
+ [key, secret] = key.split '|', 2
+ key = atob key
+ key = (key.charCodeAt(i) for i in [0...key.length])
+ secret = atob secret
+
+ s = [0...256]
+ y = 0
+ for x in [0...256]
+ y = (y + s[i] + key[x % key.length]) % 256
+ [s[x], s[y]] = [s[y], s[x]]
+
+ x = y = 0
+ result = for z in [0...secret.length]
+ x = (x + 1) % 256
+ y = (y + s[x]) % 256
+ [s[x], s[y]] = [s[y], s[x]]
+ k = s[(s[x] + s[y]) % 256]
+ String.fromCharCode((k ^ secret.charCodeAt(z)) % 256)
+
+ key = (String.fromCharCode(key[i]) for i in [0...key.length])
+ [btoa(key.join('')), btoa(result.join(''))].join '|'
+
diff --git a/lib/client/storage/dropbox/src/pulled_changes.coffee b/lib/client/storage/dropbox/src/pulled_changes.coffee
new file mode 100644
index 00000000..0a324d24
--- /dev/null
+++ b/lib/client/storage/dropbox/src/pulled_changes.coffee
@@ -0,0 +1,100 @@
+# Wraps the result of pullChanges, describing the changes in a user's Dropbox.
+class Dropbox.PulledChanges
+ # Creates a new Dropbox.PulledChanges instance from a /delta API call result.
+ #
+ # @param {?Object} deltaInfo the parsed JSON of a /delta API call result
+ # @return {?Dropbox.PulledChanges} a Dropbox.PulledChanges instance wrapping
+ # the given information; if the parameter does not look like parsed JSON,
+ # it is returned as is
+ @parse: (deltaInfo) ->
+ if deltaInfo and typeof deltaInfo is 'object'
+ new Dropbox.PulledChanges deltaInfo
+ else
+ deltaInfo
+
+ # @property {Boolean} if true, the application should reset its copy of the
+ # user's Dropbox before applying the changes described by this instance
+ blankSlate: undefined
+
+ # @property {String} encodes a cursor in the list of changes to a user's
+ # Dropbox; a pullChanges call returns some changes at the cursor, and then
+ # advance the cursor to account for the returned changes; the new cursor is
+ # returned by pullChanges, and meant to be used by a subsequent pullChanges
+ # call
+ cursorTag: undefined
+
+ # @property {Array an array with one entry for each
+ # change to the user's Dropbox returned by a pullChanges call.
+ changes: undefined
+
+ # @property {Boolean} if true, the pullChanges call returned a subset of the
+ # available changes, and the application should repeat the call
+ # immediately to get more changes
+ shouldPullAgain: undefined
+
+ # @property {Boolean} if true, the API call will not have any more changes
+ # available in the nearby future, so the application should wait for at
+ # least 5 miuntes before issuing another pullChanges request
+ shouldBackOff: undefined
+
+ # Creates a new Dropbox.PulledChanges instance from a /delta API call result.
+ #
+ # @private
+ # This constructor is used by Dropbox.PulledChanges, and should not be called
+ # directly.
+ #
+ # @param {?Object} deltaInfo the parsed JSON of a /delta API call result
+ constructor: (deltaInfo) ->
+ @blankSlate = deltaInfo.reset or false
+ @cursorTag = deltaInfo.cursor
+ @shouldPullAgain = deltaInfo.has_more
+ @shouldBackOff = not @shouldPullAgain
+ if deltaInfo.cursor and deltaInfo.cursor.length
+ @changes = (Dropbox.PullChange.parse entry for entry in deltaInfo.entries)
+ else
+ @changes = []
+
+# Wraps a single change in a pullChanges result.
+class Dropbox.PullChange
+ # Creates a Dropbox.PullChange instance wrapping an entry in a /delta result.
+ #
+ # @param {?Object} entry the parsed JSON of a single entry in a /delta API
+ # call result
+ # @return {?Dropbox.PullChange} a Dropbox.PullChange instance wrapping the
+ # given entry of a /delta API call; if the parameter does not look like
+ # parsed JSON, it is returned as is
+ @parse: (entry) ->
+ if entry and typeof entry is 'object'
+ new Dropbox.PullChange entry
+ else
+ entry
+
+ # @property {String} the path of the changed file or folder
+ path: undefined
+
+ # @property {Boolean} if true, this change is a deletion of the file or folder
+ # at the change's path; if a folder is deleted, all its contents (files
+ # and sub-folders) were also be deleted; pullChanges might not return
+ # separate changes expressing for the files or sub-folders
+ wasRemoved: undefined
+
+ # @property {?Dropbox.Stat} a Stat instance containing updated information for
+ # the file or folder; this is null if the change is a deletion
+ stat: undefined
+
+ # Creates a Dropbox.PullChange instance wrapping an entry in a /delta result.
+ #
+ # @private
+ # This constructor is used by Dropbox.PullChange.parse, and should not be
+ # called directly.
+ #
+ # @param {?Object} entry the parsed JSON of a single entry in a /delta API
+ # call result
+ constructor: (entry) ->
+ @path = entry[0]
+ @stat = Dropbox.Stat.parse entry[1]
+ if @stat
+ @wasRemoved = false
+ else
+ @stat = null
+ @wasRemoved = true
diff --git a/lib/client/storage/dropbox/src/references.coffee b/lib/client/storage/dropbox/src/references.coffee
new file mode 100644
index 00000000..f5e62437
--- /dev/null
+++ b/lib/client/storage/dropbox/src/references.coffee
@@ -0,0 +1,86 @@
+# Wraps an URL to a Dropbox file or folder that can be publicly shared.
+class Dropbox.PublicUrl
+ # Creates a PublicUrl instance from a raw API response.
+ #
+ # @param {?Object} urlData the parsed JSON describing a public URL
+ # @param {Boolean} isDirect true if this is a direct download link, false if
+ # is a file / folder preview link
+ # @return {?Dropbox.PublicUrl} a PublicUrl instance wrapping the given public
+ # link info; parameters that don't look like parsed JSON are returned as
+ # they are
+ @parse: (urlData, isDirect) ->
+ if urlData and typeof urlData is 'object'
+ new Dropbox.PublicUrl urlData, isDirect
+ else
+ urlData
+
+ # @property {String} the public URL
+ url: undefined
+
+ # @property {Date} after this time, the URL is not usable
+ expiresAt: undefined
+
+ # @property {Boolean} true if this is a direct download URL, false for URLs to
+ # preview pages in the Dropbox web app; folders do not have direct link
+ #
+ isDirect: undefined
+
+ # @property {Boolean} true if this is URL points to a file's preview page in
+ # Dropbox, false for direct links
+ isPreview: undefined
+
+ # Creates a PublicUrl instance from a raw API response.
+ #
+ # @private
+ # This constructor is used by Dropbox.PublicUrl.parse, and should not be
+ # called directly.
+ #
+ # @param {?Object} urlData the parsed JSON describing a public URL
+ # @param {Boolean} isDirect true if this is a direct download link, false if
+ # is a file / folder preview link
+ constructor: (urlData, isDirect) ->
+ @url = urlData.url
+ @expiresAt = new Date Date.parse(urlData.expires)
+
+ if isDirect is true
+ @isDirect = true
+ else if isDirect is false
+ @isDirect = false
+ else
+ @isDirect = Date.now() - @expiresAt <= 86400000 # 1 day
+ @isPreview = !@isDirect
+
+# Reference to a file that can be used to make a copy across users' Dropboxes.
+class Dropbox.CopyReference
+ # Creates a CopyReference instance from a raw reference or API response.
+ #
+ # @param {?Object, ?String} refData the parsed JSON descring a copy
+ # reference, or the reference string
+ @parse: (refData) ->
+ if refData and (typeof refData is 'object' or typeof refData is 'string')
+ new Dropbox.CopyReference refData
+ else
+ refData
+
+ # @property {String} the raw reference, for use with Dropbox APIs
+ tag: undefined
+
+ # @property {Date} deadline for using the reference in a copy operation
+ expiresAt: undefined
+
+ # Creates a CopyReference instance from a raw reference or API response.
+ #
+ # @private
+ # This constructor is used by Dropbox.CopyReference.parse, and should not be
+ # called directly.
+ #
+ # @param {?Object, ?String} refData the parsed JSON descring a copy
+ # reference, or the reference string
+ constructor: (refData) ->
+ if typeof refData is 'object'
+ @tag = refData.copy_ref
+ @expiresAt = new Date Date.parse(refData.expires)
+ else
+ @tag = refData
+ @expiresAt = new Date()
+
diff --git a/lib/client/storage/dropbox/src/stat.coffee b/lib/client/storage/dropbox/src/stat.coffee
new file mode 100644
index 00000000..6fe45c44
--- /dev/null
+++ b/lib/client/storage/dropbox/src/stat.coffee
@@ -0,0 +1,127 @@
+# The result of stat-ing a file or directory in a user's Dropbox.
+class Dropbox.Stat
+ # Creates a Stat instance from a raw "metadata" response.
+ #
+ # @param {?Object} metadata the result of parsing JSON API responses that are
+ # called "metadata" in the API documentation
+ # @return {?Dropbox.Stat} a Stat instance wrapping the given API response;
+ # parameters that aren't parsed JSON objects are returned as they are
+ @parse: (metadata) ->
+ if metadata and typeof metadata is 'object'
+ new Dropbox.Stat metadata
+ else
+ metadata
+
+ # @property {String} the path of this file or folder, relative to the user's
+ # Dropbox or to the application's folder
+ path: null
+
+ # @property {String} the name of this file or folder
+ name: null
+
+ # @property {Boolean} if true, the file or folder's path is relative to the
+ # application's folder; otherwise, the path is relative to the user's
+ # Dropbox
+ inAppFolder: null
+
+ # @property {Boolean} if true, this Stat instance describes a folder
+ isFolder: null
+
+ # @property {Boolean} if true, this Stat instance describes a file
+ isFile: null
+
+ # @property {Boolean} if true, the file or folder described by this Stat
+ # instance was from the user's Dropbox, and was obtained by an API call
+ # that returns deleted items
+ isRemoved: null
+
+ # @property {String} name of an icon in Dropbox's icon library that most
+ # accurately represents this file or folder
+ #
+ # See the Dropbox API documentation to obtain the Dropbox icon library.
+ # https://www.dropbox.com/developers/reference/api#metadata
+ typeIcon: null
+
+ # @property {String} an identifier for the contents of the described file or
+ # directories; this can used to be restored a file's contents to a
+ # previous version, or to save bandwidth by not retrieving the same
+ # folder contents twice
+ versionTag: null
+
+ # @property {String} a guess of the MIME type representing the file or folder's
+ # contents
+ mimeType: null
+
+ # @property {Number} the size of the file, in bytes; null for folders
+ size: null
+
+ # @property {String} the size of the file, in a human-readable format, such as
+ # "225.4KB"; the format of this string is influenced by the API client's
+ # locale
+ humanSize: null
+
+ # @property {Boolean} if false, the URL generated by thumbnailUrl does not
+ # point to a valid image, and should not be used
+ hasThumbnail: null
+
+ # @property {Date} the file or folder's last modification time
+ modifiedAt: null
+
+ # @property {?Date} the file or folder's last modification time, as reported
+ # by the Dropbox client that uploaded the file; this time should not be
+ # trusted, but can be used for UI (display, sorting); null if the server
+ # does not report any time
+ clientModifiedAt: null
+
+ # Creates a Stat instance from a raw "metadata" response.
+ #
+ # @private
+ # This constructor is used by Dropbox.Stat.parse, and should not be called
+ # directly.
+ #
+ # @param {Object} metadata the result of parsing JSON API responses that are
+ # called "metadata" in the API documentation
+ constructor: (metadata) ->
+ @path = metadata.path
+ # Ensure there is a trailing /, to make path processing reliable.
+ @path = '/' + @path if @path.substring(0, 1) isnt '/'
+ # Strip any trailing /, to make path joining predictable.
+ lastIndex = @path.length - 1
+ if lastIndex >= 0 and @path.substring(lastIndex) is '/'
+ @path = @path.substring 0, lastIndex
+
+ nameSlash = @path.lastIndexOf '/'
+ @name = @path.substring nameSlash + 1
+
+ @isFolder = metadata.is_dir || false
+ @isFile = !@isFolder
+ @isRemoved = metadata.is_deleted || false
+ @typeIcon = metadata.icon
+ if metadata.modified?.length
+ @modifiedAt = new Date Date.parse(metadata.modified)
+ else
+ @modifiedAt = null
+ if metadata.client_mtime?.length
+ @clientModifiedAt = new Date Date.parse(metadata.client_mtime)
+ else
+ @clientModifiedAt = null
+
+ switch metadata.root
+ when 'dropbox'
+ @inAppFolder = false
+ when 'app_folder'
+ @inAppFolder = true
+ else
+ # New "root" value that we're not aware of.
+ @inAppFolder = null
+
+ @size = metadata.bytes or 0
+ @humanSize = metadata.size or ''
+ @hasThumbnail = metadata.thumb_exists or false
+
+ if @isFolder
+ @versionTag = metadata.hash
+ @mimeType = metadata.mime_type || 'inode/directory'
+ else
+ @versionTag = metadata.rev
+ @mimeType = metadata.mime_type || 'application/octet-stream'
diff --git a/lib/client/storage/dropbox/src/user_info.coffee b/lib/client/storage/dropbox/src/user_info.coffee
new file mode 100644
index 00000000..fa61f212
--- /dev/null
+++ b/lib/client/storage/dropbox/src/user_info.coffee
@@ -0,0 +1,79 @@
+# Information about a Dropbox user.
+class Dropbox.UserInfo
+ # Creates a UserInfo instance from a raw API response.
+ #
+ # @param {?Object} userInfo the result of parsing a JSON API response that
+ # describes a user
+ # @return {Dropbox.UserInfo} a UserInfo instance wrapping the given API
+ # response; parameters that aren't parsed JSON objects are returned as
+ # the are
+ @parse: (userInfo) ->
+ if userInfo and typeof userInfo is 'object'
+ new Dropbox.UserInfo userInfo
+ else
+ userInfo
+
+ # @property {String} the user's name, in a form that is fit for display
+ name: null
+
+ # @property {?String} the user's email; this is not in the official API
+ # documentation, so it might not be supported
+ email: null
+
+ # @property {?String} two-letter country code, or null if unavailable
+ countryCode: null
+
+ # @property {String} unique ID for the user; this ID matches the unique ID
+ # returned by the authentication process
+ uid: null
+
+ # @property {String}
+ referralUrl: null
+
+ # Specific to applications whose access type is "public app folder".
+ #
+ # @property {String} prefix for URLs to the application's files
+ publicAppUrl: null
+
+ # @property {Number} the maximum amount of bytes that the user can store
+ quota: null
+
+ # @property {Number} the number of bytes taken up by the user's data
+ usedQuota: null
+
+ # @property {Number} the number of bytes taken up by the user's data that is
+ # not shared with other users
+ privateBytes: null
+
+ # @property {Number} the number of bytes taken up by the user's data that is
+ # shared with other users
+ sharedBytes: null
+
+ # Creates a UserInfo instance from a raw API response.
+ #
+ # @private
+ # This constructor is used by Dropbox.UserInfo.parse, and should not be
+ # called directly.
+ #
+ # @param {?Object} userInfo the result of parsing a JSON API response that
+ # describes a user
+ constructor: (userInfo) ->
+ @name = userInfo.display_name
+ @email = userInfo.email
+ @countryCode = userInfo.country or null
+ @uid = userInfo.uid.toString()
+ if userInfo.public_app_url
+ @publicAppUrl = userInfo.public_app_url
+ lastIndex = @publicAppUrl.length - 1
+ # Strip any trailing /, to make path joining predictable.
+ if lastIndex >= 0 and @publicAppUrl.substring(lastIndex) is '/'
+ @publicAppUrl = @publicAppUrl.substring 0, lastIndex
+ else
+ @publicAppUrl = null
+
+ @referralUrl = userInfo.referral_link
+ @quota = userInfo.quota_info.quota
+ @privateBytes = userInfo.quota_info.normal or 0
+ @sharedBytes = userInfo.quota_info.shared or 0
+ @usedQuota = @privateBytes + @sharedBytes
+
diff --git a/lib/client/storage/dropbox/src/xhr.coffee b/lib/client/storage/dropbox/src/xhr.coffee
new file mode 100644
index 00000000..ef630f67
--- /dev/null
+++ b/lib/client/storage/dropbox/src/xhr.coffee
@@ -0,0 +1,450 @@
+if window?
+ if window.XDomainRequest and not ('withCredentials' of new XMLHttpRequest())
+ DropboxXhrRequest = window.XDomainRequest
+ DropboxXhrIeMode = true
+ # IE's XDR doesn't allow setting requests' Content-Type to anything other
+ # than text/plain, so it can't send _any_ forms.
+ DropboxXhrCanSendForms = false
+ else
+ DropboxXhrRequest = window.XMLHttpRequest
+ DropboxXhrIeMode = false
+ # Firefox doesn't support adding named files to FormData.
+ # https://bugzilla.mozilla.org/show_bug.cgi?id=690659
+ DropboxXhrCanSendForms =
+ window.navigator.userAgent.indexOf('Firefox') is -1
+ DropboxXhrDoesPreflight = true
+else
+ # Node.js needs an adapter for the XHR API.
+ DropboxXhrRequest = require('xmlhttprequest').XMLHttpRequest
+ DropboxXhrIeMode = false
+ # Node.js doesn't have FormData. We wouldn't want to bother putting together
+ # upload forms in node.js anyway, because it doesn't do CORS preflight
+ # checks, so we can use PUT requests without a performance hit.
+ DropboxXhrCanSendForms = false
+ # Node.js is a server so it doesn't do annoying browser checks.
+ DropboxXhrDoesPreflight = false
+
+# ArrayBufferView isn't available in the global namespce.
+#
+# Using the hack suggested in
+# https://code.google.com/p/chromium/issues/detail?id=60449
+if typeof Uint8Array is 'undefined'
+ DropboxXhrArrayBufferView = null
+else
+ DropboxXhrArrayBufferView =
+ (new Uint8Array(0)).__proto__.__proto__.constructor
+
+# Dispatches low-level AJAX calls (XMLHttpRequests).
+class Dropbox.Xhr
+ # The object used to perform AJAX requests (XMLHttpRequest).
+ @Request = DropboxXhrRequest
+ # Set to true when using the XDomainRequest API.
+ @ieMode = DropboxXhrIeMode
+ # Set to true if the platform has proper support for FormData.
+ @canSendForms = DropboxXhrCanSendForms
+ # Set to true if the platform performs CORS preflight checks.
+ @doesPreflight = DropboxXhrDoesPreflight
+ @ArrayBufferView = DropboxXhrArrayBufferView
+
+ # Sets up an AJAX request.
+ #
+ # @param {String} method the HTTP method used to make the request ('GET',
+ # 'POST', 'PUT', etc.)
+ # @param {String} baseUrl the URL that receives the request; this URL might
+ # be modified, e.g. by appending parameters for GET requests
+ constructor: (@method, baseUrl) ->
+ @isGet = @method is 'GET'
+ @url = baseUrl
+ @headers = {}
+ @params = null
+ @body = null
+ @preflight = not (@isGet or (@method is 'POST'))
+ @signed = false
+ @responseType = null
+ @callback = null
+ @xhr = null
+
+ # Sets the parameters (form field values) that will be sent with the request.
+ #
+ # @param {?Object} params an associative array (hash) containing the HTTP
+ # request parameters
+ # @return {Dropbox.Xhr} this, for easy call chaining
+ setParams: (params) ->
+ if @signed
+ throw new Error 'setParams called after addOauthParams or addOauthHeader'
+ if @params
+ throw new Error 'setParams cannot be called twice'
+ @params = params
+ @
+
+ # Sets the function called when the XHR completes.
+ #
+ # This function can also be set when calling Dropbox.Xhr#send.
+ #
+ # @param {function(?Dropbox.ApiError, ?Object, ?Object)} callback called when
+ # the XHR completes; if an error occurs, the first parameter will be a
+ # Dropbox.ApiError instance; otherwise, the second parameter will be an
+ # instance of the required response type (e.g., String, Blob), and the
+ # third parameter will be the JSON-parsed 'x-dropbox-metadata' header
+ # @return {Dropbox.Xhr} this, for easy call chaining
+ setCallback: (@callback) ->
+ @
+
+ # Ammends the request parameters to include an OAuth signature.
+ #
+ # The OAuth signature will become invalid if the parameters are changed after
+ # the signing process.
+ #
+ # This method automatically decides the best way to add the OAuth signature
+ # to the current request. Modifying the request in any way (e.g., by adding
+ # headers) might result in a valid signature that is applied in a sub-optimal
+ # fashion. For best results, call this right before Dropbox.Xhr#prepare.
+ #
+ signWithOauth: (oauth) ->
+ if Dropbox.Xhr.ieMode or (Dropbox.Xhr.doesPreflight and (not @preflight))
+ @addOauthParams oauth
+ else
+ @addOauthHeader oauth
+
+ # Ammends the request parameters to include an OAuth signature.
+ #
+ # The OAuth signature will become invalid if the parameters are changed after
+ # the signing process.
+ #
+ # @param {Dropbox.Oauth} oauth OAuth instance whose key and secret will be
+ # used to sign the request
+ # @return {Dropbox.Xhr} this, for easy call chaining
+ addOauthParams: (oauth) ->
+ if @signed
+ throw new Error 'Request already has an OAuth signature'
+
+ @params or= {}
+ oauth.addAuthParams @method, @url, @params
+ @signed = true
+ @
+
+ # Adds an Authorize header containing an OAuth signature.
+ #
+ # The OAuth signature will become invalid if the parameters are changed after
+ # the signing process.
+ #
+ # @param {Dropbox.Oauth} oauth OAuth instance whose key and secret will be
+ # used to sign the request
+ # @return {Dropbox.Xhr} this, for easy call chaining
+ addOauthHeader: (oauth) ->
+ if @signed
+ throw new Error 'Request already has an OAuth signature'
+
+ @params or= {}
+ @signed = true
+ @setHeader 'Authorization', oauth.authHeader(@method, @url, @params)
+
+ # Sets the body (piece of data) that will be sent with the request.
+ #
+ # @param {String, Blob, ArrayBuffer} body the body to be sent in a request;
+ # GET requests cannot have a body
+ # @return {Dropbox.Xhr} this, for easy call chaining
+ setBody: (body) ->
+ if @isGet
+ throw new Error 'setBody cannot be called on GET requests'
+ if @body isnt null
+ throw new Error 'Request already has a body'
+
+ unless @preflight
+ unless (typeof FormData isnt 'undefined') and (body instanceof FormData)
+ @preflight = true
+
+ @body = body
+ @
+
+ # Sends off an AJAX request and requests a custom response type.
+ #
+ # This method requires XHR Level 2 support, which is not available in IE
+ # versions <= 9. If these browsers must be supported, it is recommended to
+ # check whether window.Blob is truthy.
+ #
+ # @param {String} responseType the value that will be assigned to the XHR's
+ # responseType property
+ # @return {Dropbox.Xhr} this, for easy call chaining
+ setResponseType: (@responseType) ->
+ @
+
+ # Sets the value of a custom HTTP header.
+ #
+ # Custom HTTP headers require a CORS preflight in browsers, so requests that
+ # use them will take more time to complete, especially on high-latency mobile
+ # connections.
+ #
+ # @param {String} headerName the name of the HTTP header
+ # @param {String} value the value that the header will be set to
+ # @return {Dropbox.Xhr} this, for easy call chaining
+ setHeader: (headerName, value) ->
+ if @headers[headerName]
+ oldValue = @headers[headerName]
+ throw new Error "HTTP header #{headerName} already set to #{oldValue}"
+ if headerName is 'Content-Type'
+ throw new Error 'Content-Type is automatically computed based on setBody'
+ @preflight = true
+ @headers[headerName] = value
+ @
+
+ # Simulates having an being sent with the request.
+ #
+ # @param {String} fieldName the name of the form field / parameter (not of
+ # the uploaded file)
+ # @param {String} fileName the name of the uploaded file (not the name of the
+ # form field / parameter)
+ # @param {String, Blob, File} fileData contents of the file to be uploaded
+ # @param {?String} contentType the MIME type of the file to be uploaded; if
+ # fileData is a Blob or File, its MIME type is used instead
+ setFileField: (fieldName, fileName, fileData, contentType) ->
+ if @body isnt null
+ throw new Error 'Request already has a body'
+
+ if @isGet
+ throw new Error 'paramsToBody cannot be called on GET requests'
+
+ if typeof(fileData) is 'object' and typeof Blob isnt 'undefined'
+ if ArrayBuffer? and fileData instanceof ArrayBuffer
+ fileData = new Uint8Array fileData
+ if Dropbox.Xhr.ArrayBufferView and
+ fileData instanceof Dropbox.Xhr.ArrayBufferView
+ contentType or= 'application/octet-stream'
+ fileData = new Blob [fileData], type: contentType
+ # Workaround for http://crbug.com/165095
+ if typeof File isnt 'undefined' and fileData instanceof File
+ fileData = new Blob [fileData], type: fileData.type
+ #fileData = fileData
+ useFormData = fileData instanceof Blob
+ else
+ useFormData = false
+
+ if useFormData
+ @body = new FormData()
+ @body.append fieldName, fileData, fileName
+ else
+ contentType or= 'application/octet-stream'
+ boundary = @multipartBoundary()
+ @headers['Content-Type'] = "multipart/form-data; boundary=#{boundary}"
+ @body = ['--', boundary, "\r\n",
+ 'Content-Disposition: form-data; name="', fieldName,
+ '"; filename="', fileName, "\"\r\n",
+ 'Content-Type: ', contentType, "\r\n",
+ "Content-Transfer-Encoding: binary\r\n\r\n",
+ fileData,
+ "\r\n", '--', boundary, '--', "\r\n"].join ''
+
+ # @private
+ # @return {String} a nonce suitable for use as a part boundary in a multipart
+ # MIME message
+ multipartBoundary: ->
+ [Date.now().toString(36), Math.random().toString(36)].join '----'
+
+ # Moves this request's parameters to its URL.
+ #
+ # @private
+ # @return {Dropbox.Xhr} this, for easy call chaining
+ paramsToUrl: ->
+ if @params
+ queryString = Dropbox.Xhr.urlEncode @params
+ if queryString.length isnt 0
+ @url = [@url, '?', queryString].join ''
+ @params = null
+ @
+
+ # Moves this request's parameters to its body.
+ #
+ # @private
+ # @return {Dropbox.Xhr} this, for easy call chaining
+ paramsToBody: ->
+ if @params
+ if @body isnt null
+ throw new Error 'Request already has a body'
+ if @isGet
+ throw new Error 'paramsToBody cannot be called on GET requests'
+ @headers['Content-Type'] = 'application/x-www-form-urlencoded'
+ @body = Dropbox.Xhr.urlEncode @params
+ @params = null
+ @
+
+ # Sets up an XHR request.
+ #
+ # This method completely sets up a native XHR object and stops short of
+ # calling its send() method, so the API client has a chance of customizing
+ # the XHR. After customizing the XHR, Dropbox.Xhr#send should be called.
+ #
+ #
+ # @return {Dropbox.Xhr} this, for easy call chaining
+ prepare: ->
+ ieMode = Dropbox.Xhr.ieMode
+ if @isGet or @body isnt null or ieMode
+ @paramsToUrl()
+ if @body isnt null and typeof @body is 'string'
+ @headers['Content-Type'] = 'text/plain; charset=utf8'
+ else
+ @paramsToBody()
+
+ @xhr = new Dropbox.Xhr.Request()
+ if ieMode
+ @xhr.onload = => @onLoad()
+ @xhr.onerror = => @onError()
+ else
+ @xhr.onreadystatechange = => @onReadyStateChange()
+ @xhr.open @method, @url, true
+
+ unless ieMode
+ for own header, value of @headers
+ @xhr.setRequestHeader header, value
+
+ if @responseType
+ if @responseType is 'b'
+ if @xhr.overrideMimeType
+ @xhr.overrideMimeType 'text/plain; charset=x-user-defined'
+ else
+ @xhr.responseType = @responseType
+
+ @
+
+ # Fires off the prepared XHR request.
+ #
+ # Dropbox.Xhr#prepare should be called exactly once before this method.
+ #
+ # @param {function(?Dropbox.ApiError, ?Object, ?Object)} callback called when
+ # the XHR completes; if an error occurs, the first parameter will be a
+ # Dropbox.ApiError instance; otherwise, the second parameter will be an
+ # instance of the required response type (e.g., String, Blob), and the
+ # third parameter will be the JSON-parsed 'x-dropbox-metadata' header
+ # @return {Dropbox.Xhr} this, for easy call chaining
+ send: (callback) ->
+ @callback = callback or @callback
+
+ if @body isnt null
+ body = @body
+ # send() in XHR doesn't like naked ArrayBuffers
+ if Dropbox.Xhr.ArrayBufferView and body instanceof ArrayBuffer
+ body = new Uint8Array body
+
+ try
+ @xhr.send body
+ catch e
+ # Firefox doesn't support sending ArrayBufferViews.
+ # Node.js doesn't implement Blob.
+ if typeof Blob isnt 'undefined' and Dropbox.Xhr.ArrayBufferView and
+ body instanceof Dropbox.Xhr.ArrayBufferView
+ body = new Blob [body], type: 'application/octet-stream'
+ @xhr.send body
+ else
+ throw e
+ else
+ @xhr.send()
+ @
+
+ # Encodes an associative array (hash) into a x-www-form-urlencoded String.
+ #
+ # For consistency, the keys are sorted in alphabetical order in the encoded
+ # output.
+ #
+ # @param {Object} object the JavaScript object whose keys will be encoded
+ # @return {String} the object's keys and values, encoded using
+ # x-www-form-urlencoded
+ @urlEncode: (object) ->
+ chunks = []
+ for key, value of object
+ chunks.push @urlEncodeValue(key) + '=' + @urlEncodeValue(value)
+ chunks.sort().join '&'
+
+ # Encodes an object into a x-www-form-urlencoded key or value.
+ #
+ # @param {Object} object the object to be encoded; the encoding calls
+ # toString() on the object to obtain its string representation
+ # @return {String} encoded string, suitable for use as a key or value in an
+ # x-www-form-urlencoded string
+ @urlEncodeValue: (object) ->
+ encodeURIComponent(object.toString()).replace(/\!/g, '%21').
+ replace(/'/g, '%27').replace(/\(/g, '%28').replace(/\)/g, '%29').
+ replace(/\*/g, '%2A')
+
+ # Decodes an x-www-form-urlencoded String into an associative array (hash).
+ #
+ # @param {String} string the x-www-form-urlencoded String to be decoded
+ # @return {Object} an associative array whose keys and values are all strings
+ @urlDecode: (string) ->
+ result = {}
+ for token in string.split '&'
+ kvp = token.split '='
+ result[decodeURIComponent(kvp[0])] = decodeURIComponent kvp[1]
+ result
+
+ # Handles the XHR readystate event.
+ onReadyStateChange: ->
+ return true if @xhr.readyState isnt 4 # XMLHttpRequest.DONE is 4
+
+ if @xhr.status < 200 or @xhr.status >= 300
+ apiError = new Dropbox.ApiError @xhr, @method, @url
+ @callback apiError
+ return true
+
+ metadataJson = @xhr.getResponseHeader 'x-dropbox-metadata'
+ if metadataJson?.length
+ try
+ metadata = JSON.parse metadataJson
+ catch e
+ # Make sure the app doesn't crash if the server goes crazy.
+ metadata = undefined
+ else
+ metadata = undefined
+
+ if @responseType
+ if @responseType is 'b'
+ dirtyText = if @xhr.responseText?
+ @xhr.responseText
+ else
+ @xhr.response
+ ###
+ jsString = ['["']
+ for i in [0...dirtyText.length]
+ hexByte = (dirtyText.charCodeAt(i) & 0xFF).toString(16)
+ if hexByte.length is 2
+ jsString.push "\\u00#{hexByte}"
+ else
+ jsString.push "\\u000#{hexByte}"
+ jsString.push '"]'
+ console.log jsString
+ text = JSON.parse(jsString.join(''))[0]
+ ###
+ bytes = []
+ for i in [0...dirtyText.length]
+ bytes.push String.fromCharCode(dirtyText.charCodeAt(i) & 0xFF)
+ text = bytes.join ''
+ @callback null, text, metadata
+ else
+ @callback null, @xhr.response, metadata
+ return true
+
+ text = if @xhr.responseText? then @xhr.responseText else @xhr.response
+ switch @xhr.getResponseHeader('Content-Type')
+ when 'application/x-www-form-urlencoded'
+ @callback null, Dropbox.Xhr.urlDecode(text), metadata
+ when 'application/json', 'text/javascript'
+ @callback null, JSON.parse(text), metadata
+ else
+ @callback null, text, metadata
+ true
+
+ # Handles the XDomainRequest onload event. (IE 8, 9)
+ onLoad: ->
+ text = @xhr.responseText
+ switch @xhr.contentType
+ when 'application/x-www-form-urlencoded'
+ @callback null, Dropbox.Xhr.urlDecode(text), undefined
+ when 'application/json', 'text/javascript'
+ @callback null, JSON.parse(text), undefined
+ else
+ @callback null, text, undefined
+ true
+
+ # Handles the XDomainRequest onload event. (IE 8, 9)
+ onError: ->
+ apiError = new Dropbox.ApiError @xhr, @method, @url
+ @callback apiError
+ return true
diff --git a/lib/client/storage/dropbox/src/zzz-export.coffee b/lib/client/storage/dropbox/src/zzz-export.coffee
new file mode 100644
index 00000000..45e47036
--- /dev/null
+++ b/lib/client/storage/dropbox/src/zzz-export.coffee
@@ -0,0 +1,21 @@
+# All changes to the global namespace happen here.
+
+# This file's name is set up in such a way that it will always show up last in
+# the source directory. This makes coffee --join work as intended.
+
+if module?.exports?
+ # We're a node.js module, so export the Dropbox class.
+ module.exports = Dropbox
+else if window?
+ # We're in a browser, so add Dropbox to the global namespace.
+ window.Dropbox = Dropbox
+else
+ throw new Error('This library only supports node.js and modern browsers.')
+
+# These are mostly useful for testing. Clients shouldn't use internal stuff.
+Dropbox.atob = atob
+Dropbox.btoa = btoa
+Dropbox.hmac = base64HmacSha1
+Dropbox.sha1 = base64Sha1
+Dropbox.encodeKey = dropboxEncodeKey
+
diff --git a/lib/client/storage/dropbox/txt.vimrc.txt b/lib/client/storage/dropbox/txt.vimrc.txt
new file mode 100644
index 00000000..b6779843
--- /dev/null
+++ b/lib/client/storage/dropbox/txt.vimrc.txt
@@ -0,0 +1,6 @@
+" Indentation settings for the project: 2-space indentation, no tabs.
+set tabstop=2
+set softtabstop=2
+set shiftwidth=2
+set expandtab
+