diff --git a/README.md b/README.md index 7d4d5ec..8251b67 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ Here we upload a file and remotely move it around before deleting it. dropbox.getAccessToken(email, pwd, function (err, token, secret) { // Upload foo.txt to the Dropbox root directory. - dropbox.putFile('foo.txt', '', function (err, data) { + dropbox.putFile('foo.txt', 'foo.txt', function (err, data) { if (err) return console.error(err) // Move it into the Public directory. @@ -97,6 +97,94 @@ For example, here we fetch the metadata about the Dropbox root directory, passin dropbox.getMetadata('', { token: token, secret: secret }, callback) +## API + +### new DropboxClient() + +### DropboxClient#getAccessToken(email, password, callback(err, access_token, access_token_secret)) + +Fetches an access token and secret based on the email and password given. Stores the token and secret in the DropboxClient instance and calls the callback them. + +### DropboxClient#getAccountInfo([optargs], callback(err, accountInfo)) +https://www.dropbox.com/developers/reference/api#account-info + +Gets account information from the client. + +### DropboxClient#createAccount(email, first_name, last_name, password, [optargs], callback(err, accountInfo)) + +Creates a new Dropbox account. + +### DropboxClient#getFile(path, [optargs], [callback(err, body)]) +https://www.dropbox.com/developers/reference/api#files-GET + +Retrieves a file specified by the path. `callback` will be called with a possible error and the buffer of the contents of the file. This method also returns a readable stream that can be used to pipe the contents. + +```js +dropboxClient('mybigfile.mpeg').pipe(fs.createWriteStream('localbigfile.mpeg'); +``` + +`optargs` can also have a `rev` field to specify the revision of the file to download, and `range` for [HTTP Range Retrieval Requests](http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.2). + +```js +// download the first 1024 byte +dropboxClient('file.zip', { range: 'bytes=0-1024'}, function(err, data) { + console.log(data.length); // 1024. that is if the file is at least 1024 bytes +}); +``` + +### DropboxClient#putFile(filepath, remotepath, [optargs], callback(err, metadata)) +https://www.dropbox.com/developers/reference/api#files_put + +Uploads a file specified by `filepath` to `remotepath` on Dropbox. Dropbox currently does not support streaming uploads, and the max upload is 150 MB. `optargs` can also take additional fields `overwrite` and `parent_rev`. + +### DropboxClient#put(contents, remotepath, [optargs], callback(err, metadata)) +o +Similar to `putFile()` but places `contents` into a created file at `remotepath`. `contents` can be a buffer or string. + +### DropboxClient#getMetadata(path, [optargs], callback(err, metadata)) +https://www.dropbox.com/developers/reference/api#metadata + +Gets metadata of file/folder specified by `path`. `optargs` can have fields `hash`, `list`, `include_deleted` and `rev`. + +### DropboxClient#delta([cursor], [optargs], callback(err, changes)) +https://www.dropbox.com/developers/reference/api#delta + +Keeps up with changes from a client's Dropbox. `changes` is an array of arrays with first element as the path and second as metadata. + +### DropboxClient#changesStream([startingCursor], [optargs]) +Convenient method that provides a more friendly API to `delta()`. Returns an event emitter that emits `data` events with `path` and `metadata` parameters on changes to the client's Dropbox. Also can emit `reset` and `error` events. The returned event emitter also has a `pause()` and `resume()` methods. + +### DropboxClient#search(folderpath, query, [optargs], callback(err, results)) +https://www.dropbox.com/developers/reference/api#search + +Searches `folderpath` for files matching `query`. `results` is an array of metadata. `optargs` can take `file_limit` and `include_deleted`. + +### DropboxClient#getThumbnail(filepath, [optargs], [callback(err, body, metadata)]) +https://www.dropbox.com/developers/reference/api#thumbnails + +Downloads a thumbnail image located at `filepath`. Like `getFile()`, the `callback` can get buffered data or the returned readable stream can be piped. `optargs` can take `format` and `size` fields. + +### DropboxClient#copy(from_path, to_path, [optargs], callback) +https://www.dropbox.com/developers/reference/api#fileops-copy + +Copies a file. `from_copy_ref` field can be given in `optargs` to use it instead of `from_path`. + +### DropboxClient#createFolder(path, [optargs], callback(err, metadata)) +https://www.dropbox.com/developers/reference/api#fileops-create-folder + +Creates a folder at the given path. + +### DropboxClient#deleteItem(path, [optargs], callback(err, metadata)) +https://www.dropbox.com/developers/reference/api#fileops-delete + +Deletes file or folder from path. + +### DropboxClient#move(from_path, to_path, [optargs], callback(err, metadata)) +https://www.dropbox.com/developers/reference/api#fileops-move + +Moves a file to another path. + + ## Testing dropbox-node depends on [jasmine-node](http://github.com/mhevery/jasmine-node) for testing. Note that the currently-implemented tests are trivial, due to a lack of a way to effectively mock the Dropbox API. diff --git a/lib/dropbox-node.js b/lib/dropbox-node.js index 12c5c33..e683cd6 100644 --- a/lib/dropbox-node.js +++ b/lib/dropbox-node.js @@ -1,8 +1,11 @@ var pathLib = require('path') , querystring = require('querystring') + , fs = require('fs') , escapePath = require('./util/escape-path') , stringifyParams = require('./util/stringify-params') - , OAuth = require('oauth').OAuth + , errors = require('./errors') + , EventEmitter = require('events').EventEmitter + , request = require('request') , API_URI = 'https://api.dropbox.com/1' , CONTENT_API_URI = 'https://api-content.dropbox.com/1'; @@ -14,48 +17,97 @@ var DropboxClient = exports.DropboxClient = this.consumer_secret = consumer_secret; this.access_token = access_token || undefined; this.access_token_secret = access_token_secret || undefined; - this.oauth = new OAuth(API_URI + '/oauth/request_token' - , API_URI + '/oauth/access_token' - , consumer_key, consumer_secret - , '1.0', null, 'HMAC-SHA1'); } +// Creates a request to the API with OAuth credentials +DropboxClient.prototype.request = + function(method, uri, optargs, body, callback) { + if (typeof body === 'function') callback = body, body = undefined; + optargs = optargs || {}; + var oauth = { + consumer_key: this.consumer_key + , consumer_secret: this.consumer_secret + , token: optargs.token || this.access_token + , token_secret: optargs.secret || this.access_token_secret + }; + + var requestOptions = { uri: uri, oauth: oauth }; + if (body) { + if (method === 'get') { + requestOptions.headers = { Range: body }; + } else { + requestOptions.body = body; + } + } + + return request[method](requestOptions, callback ? + function(err, res, body) { + if (err) return callback(err); + var contentType = res.headers['content-type']; + + // check if the response body is in JSON format + if (contentType === 'application/json' || + contentType === 'text/javascript') { + body = JSON.parse(body); + if (body.error) { + var err = new Error(body.error); + err.statusCode = res.statusCode; + return callback(err); + } + + } else if (errors[res.statusCode]) { + var err = new Error(errors[res.statusCode]); + err.statusCode = res.statusCode; + return callback(err); + } + + // check for metadata in headers + if (res.headers['x-dropbox-metadata']) { + var metadata = JSON.parse(res.headers['x-dropbox-metadata']); + } + + callback(null, body, metadata); + } : undefined); +}; + + +// Convenience methods +['get', 'post', 'put'].forEach(function(method) { + DropboxClient.prototype[method] = function(uri, optargs, body, callback) { + return this.request(method, uri, optargs, body, callback); + }; +}); + + // Fetches an access token and access token secret pair based on the user's // email and password. As well as being stored internally, the key pair is // returned to the callback in case the application developer requires it. DropboxClient.prototype.getAccessToken = function(email, pwd, cb) { // Validate email and pwd. - if (typeof email === 'function') cb = email, email = undefined; - else if (typeof pwd === 'function') cb = pwd, pwd = undefined; if (!email || !pwd) return cb(Error('Invalid arguments. Please provide ' + 'a valid email and password.')); + var uri = API_URI + '/token?' + + querystring.stringify({email: email, password: pwd}); var self = this; - this.oauth.get(API_URI + '/token?' + - querystring.stringify({email: email, password: pwd}) - , null, null, function(err, data, res) { - if (err) return cb(err); - // Store the key pair and fire callback. - self.access_token = JSON.parse(data).token; - self.access_token_secret = JSON.parse(data).secret; - cb(null, self.access_token, self.access_token_secret); - }); + this.get(uri, {}, function(err, data) { + if (err) return cb(err); + // Store the key pair and fire callback. + self.access_token = data.token; + self.access_token_secret = data.secret; + cb(null, data.token, data.secret); + }); } // Retrieves information about the user's account as a JSON response. DropboxClient.prototype.getAccountInfo = function(optargs, cb) { if (typeof optargs == 'function') cb = optargs, optargs = {}; - this.oauth.get(API_URI + '/account/info/' + - (optargs.status_in_response ? '?status_in_response=' + - optargs.status_in_response : '') - , optargs.token || this.access_token - , optargs.secret || this.access_token_secret - , function(err, data, res) { - if (err) return cb(err); - cb(null, JSON.parse(data)); - }); + var uri = API_URI + '/account/info/' + + (optargs.status_in_response ? '?status_in_response=' + + optargs.status_in_response : ''); + this.get(uri, optargs, cb); } @@ -70,12 +122,9 @@ DropboxClient.prototype.createAccount = function(email, first_name, last_name , password: password , status_in_response: optargs.status_in_response } - this.oauth.get(API_URI + '/account?' + - querystring.stringify(params) - , null, null, function(err, data, res) { - if (err) return cb(err); - cb(null, data); - }); + var uri = API_URI + '/account?' + + querystring.stringify(params); + this.get(uri, {}, cb); } @@ -83,54 +132,22 @@ DropboxClient.prototype.createAccount = function(email, first_name, last_name // user's Dropbox root. DropboxClient.prototype.getFile = function(path, optargs, cb) { if (typeof optargs == 'function') cb = optargs, optargs = {}; - path = escapePath(path); - this.oauth.get(CONTENT_API_URI + '/files/dropbox/' + path - , optargs.token || this.access_token - , optargs.secret || this.access_token_secret - , function(err, data, res) { - cb(err, data); - }); -} - - -// Sets up a streaming connection to fetch a file specified by path argument. -// Returns a request EventEmitter object. -DropboxClient.prototype.getFileStream = function(path, optargs) { - optargs = optargs || {}; - path = escapePath(path); - return this.oauth.get(CONTENT_API_URI + '/files/dropbox/' + path - , optargs.token || this.access_token - , optargs.secret || this.access_token_secret); + else if (!optargs) optargs = {}; + var uri = CONTENT_API_URI + '/files/dropbox/' + escapePath(path) + + (optargs.rev ? '?rev=' + optargs.rev : ''); + return this.get(uri, optargs, optargs.range, cb); } -// Uploads contents of a file specified by path argument, relative to -// application's directory. +// Uploads contents of a file specified by file to the remote path DropboxClient.prototype.putFile = function(file, path, optargs, cb) { if (typeof optargs == 'function') cb = optargs, optargs = {}; - var boundary = 'sAxIqse3tPlHqUIUI9ofVlHvtdt3tpaG' - , content_type = 'multipart/form-data; boundary=' + boundary - , self = this; - - require('fs').readFile(file, function(err, data) { + var uri = CONTENT_API_URI + '/files_put/dropbox/' + escapePath(path) + + '?' + stringifyParams(optargs); + var self = this; + fs.readFile(file, function(err, data) { if (err) return cb(err); - // Build request body. - path = escapePath(path); - var body = ['--' + boundary - , 'Content-Disposition: form-data; name=file; filename="' + file + '"' - , 'Content-Type: application/octet-stream' - , '', data.toString('binary'), '--' + boundary + '--', '' - ].join('\r\n'); - - self.oauth.post(CONTENT_API_URI + '/files/dropbox/' + path + - '?file=' + encodeURIComponent(file) - , optargs.token || self.access_token - , optargs.secret || self.access_token_secret - , body, content_type - , function(err, data, res) { - if (err) return cb(err); - cb(null, JSON.parse(data)); - }); + self.request('put', uri, optargs, data, cb); }); } @@ -138,28 +155,9 @@ DropboxClient.prototype.putFile = function(file, path, optargs, cb) { // Uploads contents to the specified path DropboxClient.prototype.put = function(content, path, optargs, cb) { if (typeof optargs == 'function') cb = optargs, optargs = {}; - var boundary = 'sAxIqse3tPlHqUIUI9ofVlHvtdt3tpaG' - , content_type = 'multipart/form-data; boundary=' + boundary - , self = this; - - // Build request body. - var file = pathLib.basename(path); - path = escapePath(pathLib.dirname(path)); - var body = ['--' + boundary - , 'Content-Disposition: form-data; name=file; filename="' + file + '"' - , 'Content-Type: application/octet-stream' - , '', content, '--' + boundary + '--', '' - ].join('\r\n'); - - self.oauth.post(CONTENT_API_URI + '/files/dropbox/' + path + - '?file=' + encodeURIComponent(file) - , optargs.token || self.access_token - , optargs.secret || self.access_token_secret - , body, content_type - , function(err, data, res) { - if (err) return cb(err); - cb(null, JSON.parse(data)); - }); + var uri = CONTENT_API_URI + '/files_put/dropbox/' + escapePath(path) + + '?' + stringifyParams(optargs); + this.put(uri, optargs, content, cb); } @@ -167,15 +165,9 @@ DropboxClient.prototype.put = function(content, path, optargs, cb) { // Dropbox root. DropboxClient.prototype.getMetadata = function(path, optargs, cb) { if (typeof optargs == 'function') cb = optargs, optargs = {}; - path = escapePath(path); - this.oauth.get(API_URI + '/metadata/dropbox/' + path + '?' + - stringifyParams(optargs) - , optargs.token || this.access_token - , optargs.secret || this.access_token_secret - , function(err, data, res) { - if (err) return cb(err); - cb(null, JSON.parse(data)); - }); + var uri = API_URI + '/metadata/dropbox/' + escapePath(path) + '?' + + stringifyParams(optargs); + this.get(uri, optargs, cb); } @@ -184,14 +176,10 @@ DropboxClient.prototype.getMetadata = function(path, optargs, cb) { // valid size specifiers. DropboxClient.prototype.getThumbnail = function(path, optargs, cb) { if (typeof optargs == 'function') cb = optargs, optargs = {}; - path = escapePath(path); - this.oauth.get(CONTENT_API_URI + '/thumbnails/dropbox/' + path + '?' + - stringifyParams(optargs) - , optargs.token || this.access_token - , optargs.secret || this.access_token_secret - , function(err, data, res) { - cb(err, data); - }); + optargs = optargs || {}; + var uri = CONTENT_API_URI + '/thumbnails/dropbox/' + escapePath(path) + '?' + + stringifyParams(optargs); + return this.get(uri, optargs, cb); } @@ -200,16 +188,14 @@ DropboxClient.prototype.getThumbnail = function(path, optargs, cb) { // of arguments. DropboxClient.prototype.copy = function(from_path, to_path, optargs, cb) { if (typeof optargs == 'function') cb = optargs, optargs = {}; - this.oauth.get(API_URI + '/fileops/copy?' + - querystring.stringify({root: 'dropbox' - , from_path: from_path - , to_path: to_path}) - , optargs.token || this.access_token - , optargs.secret || this.access_token_secret - , function(err, data, res) { - if (err) return cb(err); - cb(null, JSON.parse(data)); - }); + optargs.root = 'dropbox'; + if (!optargs.from_copy_ref) optargs.from_path = from_path; + optargs.to_path = to_path; + var uri = API_URI + '/fileops/copy?' + stringifyParams(optargs); + querystring.stringify({root: 'dropbox' + , from_path: from_path + , to_path: to_path}); + this.get(uri, optargs, cb); } @@ -218,15 +204,10 @@ DropboxClient.prototype.copy = function(from_path, to_path, optargs, cb) { // for explanation of arguments. DropboxClient.prototype.createFolder = function(path, optargs, cb) { if (typeof optargs == 'function') cb = optargs, optargs = {}; - this.oauth.get(API_URI + '/fileops/create_folder?' + - querystring.stringify({root: 'dropbox' - , path: path}) - , optargs.token || this.access_token - , optargs.secret || this.access_token_secret - , function(err, data, res) { - if (err) return cb(err); - cb(null, JSON.parse(data)); - }); + var uri = API_URI + '/fileops/create_folder?' + + querystring.stringify({root: 'dropbox' + , path: path}); + this.get(uri, optargs, cb); } @@ -235,14 +216,9 @@ DropboxClient.prototype.createFolder = function(path, optargs, cb) { // explanation of arguments. DropboxClient.prototype.deleteItem = function(path, optargs, cb) { if (typeof optargs == 'function') cb = optargs, optargs = {}; - this.oauth.get(API_URI + '/fileops/delete?' + - querystring.stringify({root: 'dropbox', path: path}) - , optargs.token || this.access_token - , optargs.secret || this.access_token_secret - , function(err, data, res) { - if (err) return cb(err); - cb(null, JSON.parse(data)); - }); + var uri = API_URI + '/fileops/delete?' + + querystring.stringify({root: 'dropbox', path: path}); + this.get(uri, optargs, cb); } @@ -251,14 +227,89 @@ DropboxClient.prototype.deleteItem = function(path, optargs, cb) { // explanation of arguments. DropboxClient.prototype.move = function(from_path, to_path, optargs, cb) { if (typeof optargs == 'function') cb = optargs, optargs = {}; - this.oauth.get(API_URI + '/fileops/move?' + - querystring.stringify({root: 'dropbox' - , from_path: from_path - , to_path: to_path}) - , optargs.token || this.access_token - , optargs.secret || this.access_token_secret - , function(err, data, res) { - if (err) return cb(err); - cb(null, JSON.parse(data)); - }); + var uri = API_URI + '/fileops/move?' + + querystring.stringify({root: 'dropbox' + , from_path: from_path + , to_path: to_path}); + this.get(uri, optargs, cb); +} + + +// Searches a folder +// See https://www.dropbox.com/developers/reference/api#search +DropboxClient.prototype.search = function(path, query, optargs, cb) { + if (typeof optargs === 'function') cb = optargs, optargs = {}; + optargs.query = query; + var uri = API_URI + '/search/dropbox/' + escapePath(path) + '?' + + stringifyParams(optargs); + this.get(uri, optargs, cb); +} + + +// Keep up with changes. +// See https://www.dropbox.com/developers/reference/api#delta +DropboxClient.prototype.delta = function(cursor, optargs, cb) { + var cursorType = typeof cursor; + if (cursorType === 'function') cb = cursor, optargs = {}; + else if (typeof optargs === 'function') { + cb = optargs; + if (cursorType === 'object') { + optargs = cursor; + cursor = null; + } else { + optargs = {} + } + } + + var uri = API_URI + '/delta' + + (cursor ? '?' + querystring.stringify({cursor: cursor}) : '') + this.post(uri, optargs, cb); +} + + +// Continously stream changes through DropboxClient#delta +// Returns an event emitter that has `pause()` and `resume()` methods. +// It emits `reset` events on detta resets, and `data` events with +// parameters `path` and `metadata` on new delta changes. +DropboxClient.prototype.changesStream = function(cursor, optargs) { + optargs = optargs || {}; + var ee = new EventEmitter(); + var iid; + var self = this; + + function getDelta() { + self.delta(cursor, optargs, function(err, data) { + if (err) { return ee.emit('error', err); } + + // only emit changes if cursor was given + if (cursor) { + if (data.reset) { + ee.emit('reset'); + } + + for (var i = 0, len = data.entries.length; i < len; i++) { + var e = data.entries[i]; + ee.emit('data', e[0], e[1]); + } + } + + cursor = data.cursor; + if (data.has_more) { + ee.resume(); + } + }); + } + + ee.resume = function() { + getDelta(); + clearInterval(iid); + iid = setInterval(getDelta, 300000); // 5 minutes + }; + ee.resume(); + + ee.pause = function() { + clearInterval(iid); + }; + + return ee; } diff --git a/lib/errors.js b/lib/errors.js new file mode 100644 index 0000000..a9da23a --- /dev/null +++ b/lib/errors.js @@ -0,0 +1,10 @@ +// Response error codes +module.exports = { + '304': 'The folder contents have not changed' +, '400': 'The extension is on Dropbox\'s ignore list.' +, '403': 'An invalid copy operation was attempted (e.g. there is already a file at the given destination, or copying a shared folder into a shared folder).' +, '404': 'The requested file or revision was not found.' +, '406': 'There are too many file entries to return.' +, '411': 'Chunked encoding was attempted for this upload, but is not supported by Dropbox.' +, '415': 'The image is invalid and cannot be converted to a thumbnail.' +} diff --git a/lib/util/escape-path.js b/lib/util/escape-path.js index f079b9e..6ddc209 100644 --- a/lib/util/escape-path.js +++ b/lib/util/escape-path.js @@ -1,7 +1,9 @@ // Escape URI-sensitive chars, but leave forward slashes alone. module.exports = function escapePath(p) { - return encodeURIComponent(p) + var p = encodeURIComponent(p) .replace(/%2F/g, '/') .replace(/\)/g, '%29') .replace(/\(/g, '%28'); + if (p[0] === '/') { p = p.slice(1); } + return p; } diff --git a/package.json b/package.json index 3183b8e..1f6f85a 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,9 @@ ], "homepage": "http://github.com/evnm/dropbox-node", "author": "Evan Meagher (http://evanmeagher.net/)", + "contributors": [ + "Roly Fentanes (https://github.com/fent)" + ], "main": "index", "repository": { "type": "git", @@ -19,7 +22,7 @@ "test": "node specs.js" }, "dependencies": { - "oauth": ">=0.8.2 <2.0.0" + "request": "2.9.x" }, "engines": { "node": "*" @@ -28,4 +31,4 @@ "lib": "./lib" }, "devDependencies": {} -} \ No newline at end of file +}