added ability to upload files to dropbox

This commit is contained in:
coderaiser 2012-12-17 10:52:33 -05:00
parent fff8f7cff2
commit 3e9ee9cfc2
31 changed files with 4034 additions and 129 deletions

View file

@ -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

View file

@ -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,

View file

@ -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...');
}
}
}
},

View file

@ -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;
})();
cloudcmd.DropBox = DropBoxStore;
})();

View file

@ -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;
})();

View file

@ -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;
})();

View file

@ -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')">

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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 `<head>`.
```html
<script src="//cdnjs.cloudflare.com/ajax/libs/dropbox.js/0.7.1/dropbox.min.js">
</script>
```
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.

View file

@ -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.

File diff suppressed because one or more lines are too long

View file

@ -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 <victor@costan.us> (http://www.costan.us)",
"license": "MIT",
"contributors": [
"Aakanksha Sarda <aaki@mit.edu>"
],
"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"
}
}

View file

@ -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.

View file

@ -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()

View file

@ -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'

File diff suppressed because it is too large Load diff

View file

@ -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 = """
<!doctype html>
<script type="text/javascript">window.close();</script>
<p>Please close this window.</p>
"""
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

View file

@ -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<Number>} 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

View file

@ -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()

View file

@ -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 '|'

View file

@ -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<Dropbox.PullChange> 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

View file

@ -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()

View file

@ -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'

View file

@ -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

View file

@ -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 <input type="file"> 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

View file

@ -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

View file

@ -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