dropbox-sdk 1.5.1 → 1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/CHANGELOG CHANGED
@@ -1,3 +1,7 @@
1
+ 1.6 (2013-07-07)
2
+ * Added OAuth 2 support (DropboxOAuth2Flow). OAuth 1 still works.
3
+ * Fixed many minor bugs.
4
+
1
5
  1.5.1 (2012-8-20)
2
6
  * Fixed packaging.
3
7
 
data/README CHANGED
@@ -1,7 +1,52 @@
1
- Getting started with the Dropbox Ruby SDK:
2
- 1. Install json and oauth ruby gems with:
3
- gem install json oauth
4
- 2. Edit cli_example.rb to to include your APP_KEY and APP_SECRET
5
- 3. Try running the example app: 'ruby cli_example.rb'.
6
- 4. See dropbox_controller.rb for an example Ruby on Rails controller. It needs an APP_KEY and APP_SECRET set too.
7
- 5. Check out the dropbox website for reference and help! http://dropbox.com/developers_beta
1
+ Dropbox Core SDK for Ruby
2
+
3
+ A Ruby library that for Dropbox's HTTP-based Core API.
4
+
5
+ https://www.dropbox.com/developers/core/docs
6
+
7
+ ----------------------------------
8
+ Setup
9
+
10
+ You can install this package using 'pip':
11
+
12
+ # pip install dropbox
13
+
14
+ ----------------------------------
15
+ Getting a Dropbox API key
16
+
17
+ You need a Dropbox API key to make API requests.
18
+ - Go to: https://dropbox.com/developers/apps
19
+ - If you've already registered an app, click on the "Options" link to see the
20
+ app's API key and secret.
21
+ - Otherwise, click "Create an app" to register an app. Choose "Full Dropbox" or
22
+ "App Folder" depending on your needs.
23
+ See: https://www.dropbox.com/developers/reference#permissions
24
+
25
+ ----------------------------------
26
+ Using the Dropbox API
27
+
28
+ Full documentation: https://www.dropbox.com/developers/core/
29
+
30
+ Before your app can access a Dropbox user's files, the user must authorize your
31
+ application using OAuth 2. Successfully completing this authorization flow
32
+ gives you an "access token" for the user's Dropbox account, which grants you the
33
+ ability to make Dropbox API calls to access their files.
34
+
35
+ - Authorization example for a web app: web_file_browser.rb
36
+ - Authorization example for a command-line tool:
37
+ https://www.dropbox.com/developers/core/start/ruby
38
+
39
+ Once you have an access token, create a DropboxClient instance and start making
40
+ API calls.
41
+
42
+ You only need to perform the authorization process once per user. Once you have
43
+ an access token for a user, save it somewhere persistent, like in a database.
44
+ The next time that user visits your app, you can skip the authorization process
45
+ and go straight to making API calls.
46
+
47
+ ----------------------------------
48
+ Running the Examples
49
+
50
+ There are example programs included in the tarball. Before you can run an
51
+ example, you need to edit the ".rb" file and put your Dropbox API app key and
52
+ secret in the "APP_KEY" and "APP_SECRET" constants.
data/cli_example.rb CHANGED
@@ -12,10 +12,6 @@ require 'pp'
12
12
  # Find this at https://www.dropbox.com/developers
13
13
  APP_KEY = ''
14
14
  APP_SECRET = ''
15
- ACCESS_TYPE = :app_folder #The two valid values here are :app_folder and :dropbox
16
- #The default is :app_folder, but your application might be
17
- #set to have full :dropbox access. Check your app at
18
- #https://www.dropbox.com/developers/apps
19
15
 
20
16
  class DropboxCLI
21
17
  LOGIN_REQUIRED = %w{put get cp mv rm ls mkdir info logout search thumbnail}
@@ -27,37 +23,28 @@ class DropboxCLI
27
23
  exit
28
24
  end
29
25
 
30
- @session = DropboxSession.new(APP_KEY, APP_SECRET)
31
26
  @client = nil
32
27
  end
33
28
 
34
29
  def login
35
- ########
36
- # Instead of going to a authorize URL, you can set a access token key and secret
37
- # from a previous session
38
- ########
39
- # @session.set_access_token('key', 'secret')
40
-
41
- if @session.authorized?
42
- puts "already logged in!"
30
+ if not @client.nil?
31
+ puts "already logged in!"
43
32
  else
33
+ web_auth = DropboxOAuth2FlowNoRedirect.new(APP_KEY, APP_SECRET)
34
+ authorize_url = web_auth.start()
35
+ puts "1. Go to: #{authorize_url}"
36
+ puts "2. Click \"Allow\" (you might have to log in first)."
37
+ puts "3. Copy the authorization code."
44
38
 
45
- # grab the request token for session
46
- @session.get_request_token
39
+ print "Enter the authorization code here: "
40
+ STDOUT.flush
41
+ auth_code = STDIN.gets.strip
47
42
 
48
- authorize_url = @session.get_authorize_url
49
- puts "Got a request token. Your request token key is #{@session.request_token.key} and your token secret is #{@session.request_token.secret}"
50
-
51
- # make the user log in and authorize this token
52
- puts "AUTHORIZING", authorize_url, "Please visit that web page and hit 'Allow', then hit Enter here."
53
- gets
54
-
55
- # get the access token from the server. Its then stored in the session.
56
- @session.get_access_token
43
+ access_token, user_id = web_auth.finish(auth_code)
57
44
 
45
+ @client = DropboxClient.new(access_token)
46
+ puts "You are logged in. Your access token is #{access_token}."
58
47
  end
59
- puts "You are logged in. Your access token key is #{@session.access_token.key} your secret is #{@session.access_token.secret}"
60
- @client = DropboxClient.new(@session, ACCESS_TYPE)
61
48
  end
62
49
 
63
50
  def command_loop
@@ -96,9 +83,8 @@ class DropboxCLI
96
83
  end
97
84
 
98
85
  def logout(command)
99
- @session.clear_access_token
100
- puts "You are logged out."
101
86
  @client = nil
87
+ puts "You are logged out."
102
88
  end
103
89
 
104
90
  def put(command)
@@ -1,57 +1,107 @@
1
- require 'dropbox_sdk'
1
+ # ---------------------------------------------------------------------------------------
2
+ # A Rails 3 controller that:
3
+ # - Runs the through Dropbox's OAuth 2 flow, yielding a Dropbox API access token.
4
+ # - Makes a Dropbox API call to upload a file.
5
+ #
6
+ # To run:
7
+ # 1. You need a Rails 3 project (to create one, run: rails new <folder-name>)
8
+ # 2. Copy this file into <folder-name>/app/controllers/
9
+ # 3. Add the following lines to <folder-name>/config/routes.rb
10
+ # get "dropbox/main"
11
+ # post "dropbox/upload"
12
+ # get "dropbox/auth_start"
13
+ # get "dropbox/auth_finish"
14
+ # 4. Run: rails server
15
+ # 5. Point your browser at: https://localhost:3000/dropbox/main
2
16
 
3
- # This is an example of a Rails 3 controller that authorizes an application
4
- # and then uploads a file to the user's Dropbox.
17
+ require '../lib/dropbox_sdk'
18
+ require 'securerandom'
5
19
 
6
- # You must set these
7
20
  APP_KEY = ""
8
21
  APP_SECRET = ""
9
- ACCESS_TYPE = :app_folder #The two valid values here are :app_folder and :dropbox
10
- #The default is :app_folder, but your application might be
11
- #set to have full :dropbox access. Check your app at
12
- #https://www.dropbox.com/developers/apps
13
22
 
23
+ class DropboxController < ApplicationController
14
24
 
15
- # Examples routes for config/routes.rb (Rails 3)
16
- #match 'db/authorize', :controller => 'db', :action => 'authorize'
17
- #match 'db/upload', :controller => 'db', :action => 'upload'
25
+ def main
26
+ client = get_dropbox_client
27
+ unless client
28
+ redirect_to(:action => 'auth_start') and return
29
+ end
18
30
 
19
- class DbController < ApplicationController
20
- def authorize
21
- if not params[:oauth_token] then
22
- dbsession = DropboxSession.new(APP_KEY, APP_SECRET)
31
+ account_info = client.account_info
23
32
 
24
- session[:dropbox_session] = dbsession.serialize #serialize and save this DropboxSession
33
+ # Show a file upload page
34
+ render :inline =>
35
+ "#{account_info['email']} <br/><%= form_tag({:action => :upload}, :multipart => true) do %><%= file_field_tag 'file' %><%= submit_tag 'Upload' %><% end %>"
36
+ end
25
37
 
26
- #pass to get_authorize_url a callback url that will return the user here
27
- redirect_to dbsession.get_authorize_url url_for(:action => 'authorize')
28
- else
29
- # the user has returned from Dropbox
30
- dbsession = DropboxSession.deserialize(session[:dropbox_session])
31
- dbsession.get_access_token #we've been authorized, so now request an access_token
32
- session[:dropbox_session] = dbsession.serialize
38
+ def upload
39
+ client = get_dropbox_client
40
+ unless client
41
+ redirect_to(:action => 'auth_start') and return
42
+ end
33
43
 
34
- redirect_to :action => 'upload'
44
+ begin
45
+ # Upload the POST'd file to Dropbox, keeping the same name
46
+ resp = client.put_file(params[:file].original_filename, params[:file].read)
47
+ render :text => "Upload successful. File now at #{resp['path']}"
48
+ rescue DropboxAuthError => e
49
+ session.delete(:access_token) # An auth error means the access token is probably bad
50
+ logger.info "Dropbox auth error: #{e}"
51
+ render :text => "Dropbox auth error"
52
+ rescue DropboxError => e
53
+ logger.info "Dropbox API error: #{e}"
54
+ render :text => "Dropbox API error"
35
55
  end
36
56
  end
37
57
 
38
- def upload
39
- # Check if user has no dropbox session...re-direct them to authorize
40
- return redirect_to(:action => 'authorize') unless session[:dropbox_session]
41
-
42
- dbsession = DropboxSession.deserialize(session[:dropbox_session])
43
- client = DropboxClient.new(dbsession, ACCESS_TYPE) #raise an exception if session not authorized
44
- info = client.account_info # look up account information
45
-
46
- if request.method != "POST"
47
- # show a file upload page
48
- render :inline =>
49
- "#{info['email']} <br/><%= form_tag({:action => :upload}, :multipart => true) do %><%= file_field_tag 'file' %><%= submit_tag %><% end %>"
50
- return
51
- else
52
- # upload the posted file to dropbox keeping the same name
53
- resp = client.put_file(params[:file].original_filename, params[:file].read)
54
- render :text => "Upload successful! File now at #{resp['path']}"
58
+ def get_dropbox_client
59
+ if session[:access_token]
60
+ begin
61
+ access_token = session[:access_token]
62
+ DropboxClient.new(access_token)
63
+ rescue
64
+ # Maybe something's wrong with the access token?
65
+ session.delete(:access_token)
66
+ raise
67
+ end
68
+ end
69
+ end
70
+
71
+ def get_web_auth()
72
+ redirect_uri = url_for(:action => 'auth_finish')
73
+ DropboxOAuth2Flow.new(APP_KEY, APP_SECRET, redirect_uri, session, :dropbox_auth_csrf_token)
74
+ end
75
+
76
+ def auth_start
77
+ authorize_url = get_web_auth().start()
78
+
79
+ # Send the user to the Dropbox website so they can authorize our app. After the user
80
+ # authorizes our app, Dropbox will redirect them here with a 'code' parameter.
81
+ redirect_to authorize_url
82
+ end
83
+
84
+ def auth_finish
85
+ begin
86
+ access_token, user_id, url_state = get_web_auth.finish(params)
87
+ session[:access_token] = access_token
88
+ redirect_to :action => 'main'
89
+ rescue DropboxOAuth2Flow::BadRequestError => e
90
+ render :text => "Error in OAuth 2 flow: Bad request: #{e}"
91
+ rescue DropboxOAuth2Flow::BadStateError => e
92
+ logger.info("Error in OAuth 2 flow: No CSRF token in session: #{e}")
93
+ redirect_to(:action => 'auth_start')
94
+ rescue DropboxOAuth2Flow::CsrfError => e
95
+ logger.info("Error in OAuth 2 flow: CSRF mismatch: #{e}")
96
+ render :text => "CSRF error"
97
+ rescue DropboxOAuth2Flow::NotApprovedError => e
98
+ render :text => "Not approved? Why not, bro?"
99
+ rescue DropboxOAuth2Flow::ProviderError => e
100
+ logger.info "Error in OAuth 2 flow: Error redirect from Dropbox: #{e}"
101
+ render :text => "Strange error."
102
+ rescue DropboxError => e
103
+ logger.info "Error getting OAuth 2 access token: #{e}"
104
+ render :text => "Error communicating with Dropbox servers."
55
105
  end
56
106
  end
57
107
  end
data/lib/dropbox_sdk.rb CHANGED
@@ -4,6 +4,9 @@ require 'net/https'
4
4
  require 'cgi'
5
5
  require 'json'
6
6
  require 'yaml'
7
+ require 'base64'
8
+ require 'securerandom'
9
+ require 'pp'
7
10
 
8
11
  module Dropbox # :nodoc:
9
12
  API_SERVER = "api.dropbox.com"
@@ -11,36 +14,17 @@ module Dropbox # :nodoc:
11
14
  WEB_SERVER = "www.dropbox.com"
12
15
 
13
16
  API_VERSION = 1
14
- SDK_VERSION = "1.5.1"
17
+ SDK_VERSION = "1.6"
15
18
 
16
19
  TRUSTED_CERT_FILE = File.join(File.dirname(__FILE__), 'trusted-certs.crt')
17
- end
18
-
19
- # DropboxSession is responsible for holding OAuth information. It knows how to take your consumer key and secret
20
- # and request an access token, an authorize url, and get an access token. You just need to pass it to
21
- # DropboxClient after its been authorized.
22
- class DropboxSession
23
-
24
- # * consumer_key - Your Dropbox application's "app key".
25
- # * consumer_secret - Your Dropbox application's "app secret".
26
- def initialize(consumer_key, consumer_secret)
27
- @consumer_key = consumer_key
28
- @consumer_secret = consumer_secret
29
- @request_token = nil
30
- @access_token = nil
31
- end
32
-
33
- private
34
20
 
35
- def do_http(uri, auth_token, request) # :nodoc:
21
+ def self.do_http(uri, request) # :nodoc:
36
22
  http = Net::HTTP.new(uri.host, uri.port)
37
23
 
38
24
  http.use_ssl = true
39
- enable_cert_checking(http)
25
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
40
26
  http.ca_file = Dropbox::TRUSTED_CERT_FILE
41
27
 
42
- request.add_field('Authorization', build_auth_header(auth_token))
43
-
44
28
  #We use this to better understand how developers are using our SDKs.
45
29
  request['User-Agent'] = "OfficialDropboxRubySDK/#{Dropbox::SDK_VERSION}"
46
30
 
@@ -48,38 +32,69 @@ class DropboxSession
48
32
  http.request(request)
49
33
  rescue OpenSSL::SSL::SSLError => e
50
34
  raise DropboxError.new("SSL error connecting to Dropbox. " +
51
- "There may be a problem with the set of certificates in \"#{Dropbox::TRUSTED_CERT_FILE}\". " +
52
- e)
35
+ "There may be a problem with the set of certificates in \"#{Dropbox::TRUSTED_CERT_FILE}\". #{e}")
53
36
  end
54
37
  end
55
38
 
56
- def enable_cert_checking(http)
57
- http.verify_mode = OpenSSL::SSL::VERIFY_PEER
39
+ # Parse response. You probably shouldn't be calling this directly. This takes responses from the server
40
+ # and parses them. It also checks for errors and raises exceptions with the appropriate messages.
41
+ def self.parse_response(response, raw=false) # :nodoc:
42
+ if response.kind_of?(Net::HTTPServerError)
43
+ raise DropboxError.new("Dropbox Server Error: #{response} - #{response.body}", response)
44
+ elsif response.kind_of?(Net::HTTPUnauthorized)
45
+ raise DropboxAuthError.new("User is not authenticated.", response)
46
+ elsif not response.kind_of?(Net::HTTPSuccess)
47
+ begin
48
+ d = JSON.parse(response.body)
49
+ rescue
50
+ raise DropboxError.new("Dropbox Server Error: body=#{response.body}", response)
51
+ end
52
+ if d['user_error'] and d['error']
53
+ raise DropboxError.new(d['error'], response, d['user_error']) #user_error is translated
54
+ elsif d['error']
55
+ raise DropboxError.new(d['error'], response)
56
+ else
57
+ raise DropboxError.new(response.body, response)
58
+ end
59
+ end
60
+
61
+ return response.body if raw
62
+
63
+ begin
64
+ return JSON.parse(response.body)
65
+ rescue JSON::ParserError
66
+ raise DropboxError.new("Unable to parse JSON response: #{response.body}", response)
67
+ end
58
68
  end
59
69
 
60
- def build_auth_header(token) # :nodoc:
61
- header = "OAuth oauth_version=\"1.0\", oauth_signature_method=\"PLAINTEXT\", " +
62
- "oauth_consumer_key=\"#{URI.escape(@consumer_key)}\", "
63
- if token
64
- key = URI.escape(token.key)
65
- secret = URI.escape(token.secret)
66
- header += "oauth_token=\"#{key}\", oauth_signature=\"#{URI.escape(@consumer_secret)}&#{secret}\""
70
+ # A string comparison function that is resistant to timing attacks. If you're comparing a
71
+ # string you got from the outside world with a string that is supposed to be a secret, use
72
+ # this function to check equality.
73
+ def self.safe_string_equals(a, b)
74
+ if a.length != b.length
75
+ false
67
76
  else
68
- header += "oauth_signature=\"#{URI.escape(@consumer_secret)}&\""
77
+ a.chars.zip(b.chars).map {|ac,bc| ac == bc}.all?
69
78
  end
70
- header
71
79
  end
80
+ end
72
81
 
73
- def do_get_with_token(url, token, headers=nil) # :nodoc:
74
- uri = URI.parse(url)
75
- do_http(uri, token, Net::HTTP::Get.new(uri.request_uri))
82
+ class DropboxSessionBase # :nodoc:
83
+
84
+ protected
85
+
86
+ def do_http(uri, request) # :nodoc:
87
+ sign_request(request)
88
+ Dropbox::do_http(uri, request)
76
89
  end
77
90
 
78
91
  public
79
92
 
80
93
  def do_get(url, headers=nil) # :nodoc:
81
94
  assert_authorized
82
- do_get_with_token(url, @access_token)
95
+ uri = URI.parse(url)
96
+ request = Net::HTTP::Get.new(uri.request_uri)
97
+ do_http(uri, request)
83
98
  end
84
99
 
85
100
  def do_http_with_body(uri, request, body)
@@ -103,7 +118,7 @@ class DropboxSession
103
118
  request.body = s
104
119
  end
105
120
  end
106
- do_http(uri, @access_token, request)
121
+ do_http(uri, request)
107
122
  end
108
123
 
109
124
  def do_post(url, headers=nil, body=nil) # :nodoc:
@@ -117,7 +132,51 @@ class DropboxSession
117
132
  uri = URI.parse(url)
118
133
  do_http_with_body(uri, Net::HTTP::Put.new(uri.request_uri, headers), body)
119
134
  end
135
+ end
120
136
 
137
+ # DropboxSession is responsible for holding OAuth 1 information. It knows how to take your consumer key and secret
138
+ # and request an access token, an authorize url, and get an access token. You just need to pass it to
139
+ # DropboxClient after its been authorized.
140
+ class DropboxSession < DropboxSessionBase # :nodoc:
141
+
142
+ # * consumer_key - Your Dropbox application's "app key".
143
+ # * consumer_secret - Your Dropbox application's "app secret".
144
+ def initialize(consumer_key, consumer_secret)
145
+ @consumer_key = consumer_key
146
+ @consumer_secret = consumer_secret
147
+ @request_token = nil
148
+ @access_token = nil
149
+ end
150
+
151
+ private
152
+
153
+ def build_auth_header(token) # :nodoc:
154
+ header = "OAuth oauth_version=\"1.0\", oauth_signature_method=\"PLAINTEXT\", " +
155
+ "oauth_consumer_key=\"#{URI.escape(@consumer_key)}\", "
156
+ if token
157
+ key = URI.escape(token.key)
158
+ secret = URI.escape(token.secret)
159
+ header += "oauth_token=\"#{key}\", oauth_signature=\"#{URI.escape(@consumer_secret)}&#{secret}\""
160
+ else
161
+ header += "oauth_signature=\"#{URI.escape(@consumer_secret)}&\""
162
+ end
163
+ header
164
+ end
165
+
166
+ def do_get_with_token(url, token, headers=nil) # :nodoc:
167
+ uri = URI.parse(url)
168
+ request = Net::HTTP::Get.new(uri.request_uri)
169
+ request.add_field('Authorization', build_auth_header(token))
170
+ Dropbox::do_http(uri, request)
171
+ end
172
+
173
+ protected
174
+
175
+ def sign_request(request) # :nodoc:
176
+ request.add_field('Authorization', build_auth_header(@access_token))
177
+ end
178
+
179
+ public
121
180
 
122
181
  def get_token(url_end, input_token, error_message_prefix) #: nodoc:
123
182
  response = do_get_with_token("https://#{Dropbox::API_SERVER}:443/#{Dropbox::API_VERSION}/oauth#{url_end}", input_token)
@@ -193,7 +252,7 @@ class DropboxSession
193
252
  return @access_token if authorized?
194
253
 
195
254
  if @request_token.nil?
196
- raise DropboxAuthError.new("No request token. You must set this or get an authorize url first.")
255
+ raise RuntimeError.new("No request token. You must set this or get an authorize url first.")
197
256
  end
198
257
 
199
258
  @access_token = get_token("/access_token", @request_token, "Couldn't get access token.")
@@ -244,6 +303,302 @@ class DropboxSession
244
303
  end
245
304
 
246
305
 
306
+ class DropboxOAuth2Session < DropboxSessionBase # :nodoc:
307
+ def initialize(oauth2_access_token)
308
+ if not oauth2_access_token.is_a?(String)
309
+ raise "bad type for oauth2_access_token (expecting String)"
310
+ end
311
+ @access_token = oauth2_access_token
312
+ end
313
+
314
+ def assert_authorized
315
+ true
316
+ end
317
+
318
+ protected
319
+
320
+ def sign_request(request) # :nodoc:
321
+ request.add_field('Authorization', 'Bearer ' + @access_token)
322
+ end
323
+ end
324
+
325
+ # Base class for the two OAuth 2 authorization helpers.
326
+ class DropboxOAuth2FlowBase # :nodoc:
327
+ def initialize(consumer_key, consumer_secret, locale=nil)
328
+ if not consumer_key.is_a?(String)
329
+ raise ArgumentError, "consumer_key must be a String, got #{consumer_key.inspect}"
330
+ end
331
+ if not consumer_secret.is_a?(String)
332
+ raise ArgumentError, "consumer_secret must be a String, got #{consumer_secret.inspect}"
333
+ end
334
+ if not (locale.nil? or locale.is_a?(String))
335
+ raise ArgumentError, "locale must be a String or nil, got #{locale.inspect}"
336
+ end
337
+ @consumer_key = consumer_key
338
+ @consumer_secret = consumer_secret
339
+ @locale = locale
340
+ end
341
+
342
+ def _get_authorize_url(redirect_uri, state)
343
+ params = {"client_id" => @consumer_key, "response_type" => "code"}
344
+ if not redirect_uri.nil?
345
+ params["redirect_uri"] = redirect_uri
346
+ end
347
+ if not state.nil?
348
+ params["state"] = state
349
+ end
350
+ if not @locale.nil?
351
+ params['locale'] = @locale
352
+ end
353
+
354
+ host = Dropbox::WEB_SERVER
355
+ path = "/#{Dropbox::API_VERSION}/oauth2/authorize"
356
+
357
+ target = URI::Generic.new("https", nil, host, nil, nil, path, nil, nil, nil)
358
+
359
+ target.query = params.collect {|k,v|
360
+ CGI.escape(k) + "=" + CGI.escape(v)
361
+ }.join("&")
362
+
363
+ target.to_s
364
+ end
365
+
366
+ # Finish the OAuth 2 authorization process. If you used a redirect_uri, pass that in.
367
+ # Will return an access token string that you can use with DropboxClient.
368
+ def _finish(code, original_redirect_uri)
369
+ if not code.is_a?(String)
370
+ raise ArgumentError, "code must be a String"
371
+ end
372
+
373
+ uri = URI.parse("https://#{Dropbox::API_SERVER}/1/oauth2/token")
374
+ request = Net::HTTP::Post.new(uri.request_uri)
375
+ client_credentials = @consumer_key + ':' + @consumer_secret
376
+ request.add_field('Authorization', 'Basic ' + Base64.encode64(client_credentials).chomp("\n"))
377
+
378
+ params = {
379
+ "grant_type" => "authorization_code",
380
+ "code" => code,
381
+ }
382
+
383
+ if not @locale.nil?
384
+ params['locale'] = @locale
385
+ end
386
+ if not original_redirect_uri.nil?
387
+ params['redirect_uri'] = original_redirect_uri
388
+ end
389
+
390
+ form_data = {}
391
+ params.each {|k,v| form_data[k.to_s] = v if !v.nil?}
392
+ request.set_form_data(form_data)
393
+
394
+ response = Dropbox::do_http(uri, request)
395
+
396
+ j = Dropbox::parse_response(response)
397
+ ["token_type", "access_token", "uid"].each { |k|
398
+ if not j.has_key?(k)
399
+ raise DropboxError.new("Bad response from /token: missing \"#{k}\".")
400
+ end
401
+ if not j[k].is_a?(String)
402
+ raise DropboxError.new("Bad response from /token: field \"#{k}\" is not a string.")
403
+ end
404
+ }
405
+ if j["token_type"] != "bearer" and j["token_type"] != "Bearer":
406
+ raise DropboxError.new("Bad response from /token: \"token_type\" is \"#{token_type}\".")
407
+ end
408
+
409
+ return j['access_token'], j['uid']
410
+ end
411
+ end
412
+
413
+ # OAuth 2 authorization helper for apps that can't provide a redirect URI
414
+ # (such as the command line example apps).
415
+ class DropboxOAuth2FlowNoRedirect < DropboxOAuth2FlowBase
416
+
417
+ # * consumer_key: Your Dropbox API app's "app key"
418
+ # * consumer_secret: Your Dropbox API app's "app secret"
419
+ # * locale: The locale of the user currently using your app.
420
+ def initialize(consumer_key, consumer_secret, locale=nil)
421
+ super(consumer_key, consumer_secret, locale)
422
+ end
423
+
424
+ # Returns a authorization_url, which is a page on Dropbox's website. Have the user
425
+ # visit this URL and approve your app.
426
+ def start()
427
+ _get_authorize_url(nil, nil)
428
+ end
429
+
430
+ # If the user approves your app, they will be presented with an "authorization code".
431
+ # Have the user copy/paste that authorization code into your app and then call this
432
+ # method to get an access token.
433
+ #
434
+ # Returns a two-entry list (access_token, user_id)
435
+ # * access_token is an access token string that can be passed to DropboxClient.
436
+ # * user_id is the Dropbox user ID of the user that just approved your app.
437
+ def finish(code)
438
+ _finish(code, nil)
439
+ end
440
+ end
441
+
442
+ # The standard OAuth 2 authorization helper. Use this if you're writing a web app.
443
+ class DropboxOAuth2Flow < DropboxOAuth2FlowBase
444
+
445
+ # * consumer_key: Your Dropbox API app's "app key"
446
+ # * consumer_secret: Your Dropbox API app's "app secret"
447
+ # * redirect_uri: The URI that the Dropbox server will redirect the user to after the user
448
+ # finishes authorizing your app. This URI must be HTTPs-based and pre-registered with
449
+ # the Dropbox servers, though localhost URIs are allowed without pre-registration and can
450
+ # be either HTTP or HTTPS.
451
+ # * session: A hash that represents the current web app session (will be used to save the CSRF
452
+ # token)
453
+ # * csrf_token_key: The key to use when storing the CSRF token in the session (for example,
454
+ # :dropbox_auth_csrf_token)
455
+ # * locale: The locale of the user currently using your app (ex: "en" or "en_US").
456
+ def initialize(consumer_key, consumer_secret, redirect_uri, session, csrf_token_session_key, locale=nil)
457
+ super(consumer_key, consumer_secret, locale)
458
+ if not redirect_uri.is_a?(String)
459
+ raise ArgumentError, "redirect_uri must be a String, got #{consumer_secret.inspect}"
460
+ end
461
+ @redirect_uri = redirect_uri
462
+ @session = session
463
+ @csrf_token_session_key = csrf_token_session_key
464
+ end
465
+
466
+ # Starts the OAuth 2 authorizaton process, which involves redirecting the user to
467
+ # the returned "authorization URL" (a URL on the Dropbox website). When the user then
468
+ # either approves or denies your app access, Dropbox will redirect them to the
469
+ # redirect_uri you provided to the constructor, at which point you should call finish()
470
+ # to complete the process.
471
+ #
472
+ # This function will also save a CSRF token to the session and csrf_token_session_key
473
+ # you provided to the constructor. This CSRF token will be checked on finish() to prevent
474
+ # request forgery.
475
+ #
476
+ # * url_state: Any data you would like to keep in the URL through the authorization
477
+ # process. This exact value will be returned to you by finish().
478
+ #
479
+ # Returns the URL to redirect the user to.
480
+ def start(url_state=nil)
481
+ unless url_state.nil? or url_state.is_a?(String)
482
+ raise ArgumentError, "url_state must be a String"
483
+ end
484
+
485
+ csrf_token = SecureRandom.base64(16)
486
+ state = csrf_token
487
+ unless url_state.nil?
488
+ state += "|" + url_state
489
+ end
490
+ @session[@csrf_token_session_key] = csrf_token
491
+
492
+ return _get_authorize_url(@redirect_uri, state)
493
+ end
494
+
495
+ # Call this after the user has visited the authorize URL (see: start()), approved your app,
496
+ # and was redirected to your redirect URI.
497
+ #
498
+ # * query_params: The query params on the GET request to your redirect URI.
499
+ #
500
+ # Returns a tuple of (access_token, user_id, url_state). access_token can be used to
501
+ # construct a DropboxClient. user_id is the Dropbox user ID of the user that jsut approved
502
+ # your app. url_state is the value you originally passed in to start().
503
+ #
504
+ # Can throw BadRequestError, BadStateError, CsrfError, NotApprovedError,
505
+ # ProviderError, and the standard DropboxError.
506
+ def finish(query_params)
507
+ csrf_token_from_session = @session[@csrf_token_session_key]
508
+
509
+ # Check well-formedness of request.
510
+
511
+ state = query_params['state']
512
+ if state.nil?
513
+ raise BadRequestError.new("Missing query parameter 'state'.")
514
+ end
515
+
516
+ error = query_params['error']
517
+ error_description = query_params['error_description']
518
+ code = query_params['code']
519
+
520
+ if not error.nil? and not code.nil?
521
+ raise BadRequestError.new("Query parameters 'code' and 'error' are both set;" +
522
+ " only one must be set.")
523
+ end
524
+ if error.nil? and code.nil?
525
+ raise BadRequestError.new("Neither query parameter 'code' or 'error' is set.")
526
+ end
527
+
528
+ # Check CSRF token
529
+
530
+ if csrf_token_from_session.nil?
531
+ raise BadStateError.new("Missing CSRF token in session.");
532
+ end
533
+ unless csrf_token_from_session.length > 20
534
+ raise RuntimeError.new("CSRF token unexpectedly short: #{csrf_token_from_session.inspect}")
535
+ end
536
+
537
+ split_pos = state.index('|')
538
+ if split_pos.nil?
539
+ given_csrf_token = state
540
+ url_state = nil
541
+ else
542
+ given_csrf_token, url_state = state.split('|', 2)
543
+ end
544
+ if not Dropbox::safe_string_equals(csrf_token_from_session, given_csrf_token)
545
+ raise CsrfError.new("Expected #{csrf_token_from_session.inspect}, " +
546
+ "got #{given_csrf_token.inspect}.")
547
+ end
548
+ @session.delete(@csrf_token_session_key)
549
+
550
+ # Check for error identifier
551
+
552
+ if not error.nil?
553
+ if error == 'access_denied'
554
+ # The user clicked "Deny"
555
+ if error_description.nil?
556
+ raise NotApprovedError.new("No additional description from Dropbox.")
557
+ else
558
+ raise NotApprovedError.new("Additional description from Dropbox: #{error_description}")
559
+ end
560
+ else
561
+ # All other errors.
562
+ full_message = error
563
+ if not error_description.nil?
564
+ full_message += ": " + error_description
565
+ end
566
+ raise ProviderError.new(full_message)
567
+ end
568
+ end
569
+
570
+ # If everything went ok, make the network call to get an access token.
571
+
572
+ access_token, user_id = _finish(code, @redirect_uri)
573
+ return access_token, user_id, url_state
574
+ end
575
+
576
+ # Thrown if the redirect URL was missing parameters or if the given parameters were not valid.
577
+ #
578
+ # The recommended action is to show an HTTP 400 error page.
579
+ class BadRequestError < Exception; end
580
+
581
+ # Thrown if all the parameters are correct, but there's no CSRF token in the session. This
582
+ # probably means that the session expired.
583
+ #
584
+ # The recommended action is to redirect the user's browser to try the approval process again.
585
+ class BadStateError < Exception; end
586
+
587
+ # Thrown if the given 'state' parameter doesn't contain the CSRF token from the user's session.
588
+ # This is blocked to prevent CSRF attacks.
589
+ #
590
+ # The recommended action is to respond with an HTTP 403 error page.
591
+ class CsrfError < Exception; end
592
+
593
+ # The user chose not to approve your app.
594
+ class NotApprovedError < Exception; end
595
+
596
+ # Dropbox redirected to your redirect URI with some unexpected error identifier and error
597
+ # message.
598
+ class ProviderError < Exception; end
599
+ end
600
+
601
+
247
602
  # A class that represents either an OAuth request token or an OAuth access token.
248
603
  class OAuthToken # :nodoc:
249
604
  def initialize(key, secret)
@@ -289,19 +644,27 @@ end
289
644
  class DropboxNotModified < DropboxError
290
645
  end
291
646
 
292
- # This is the Dropbox Client API you'll be working with most often. You need to give it
293
- # a DropboxSession which has already been authorized, or which it can authorize.
647
+ # Use this class to make Dropbox API calls. You'll need to obtain an OAuth 2 access token
648
+ # first; you can get one using either DropboxOAuth2Flow or DropboxOAuth2FlowNoRedirect.
294
649
  class DropboxClient
295
650
 
296
- # Initialize a new DropboxClient. You need to give it a session which has been authorized. See
297
- # documentation on DropboxSession for how to authorize it.
298
- def initialize(session, root="app_folder", locale=nil)
299
- session.get_access_token
651
+ # Args:
652
+ # * +oauth2_access_token+: Obtained via DropboxOAuth2Flow or DropboxOAuth2FlowNoRedirect.
653
+ # * +locale+: The user's current locale (used to localize error messages).
654
+ def initialize(oauth2_access_token, root="auto", locale=nil)
655
+ if oauth2_access_token.is_a?(String)
656
+ @session = DropboxOAuth2Session.new(oauth2_access_token)
657
+ elsif oauth2_access_token.is_a?(DropboxSession)
658
+ @session = oauth2_access_token
659
+ @session.get_access_token
660
+ else
661
+ raise ArgumentError.new("oauth2_access_token doesn't have a valid type")
662
+ end
300
663
 
301
664
  @root = root.to_s # If they passed in a symbol, make it a string
302
665
 
303
- if not ["dropbox","app_folder"].include?(@root)
304
- raise DropboxError.new("root must be :dropbox or :app_folder")
666
+ if not ["dropbox","app_folder","auto"].include?(@root)
667
+ raise ArgumentError.new("root must be :dropbox, :app_folder, or :auto")
305
668
  end
306
669
  if @root == "app_folder"
307
670
  #App Folder is the name of the access type, but for historical reasons
@@ -310,63 +673,32 @@ class DropboxClient
310
673
  end
311
674
 
312
675
  @locale = locale
313
- @session = session
314
- end
315
-
316
- # Parse response. You probably shouldn't be calling this directly. This takes responses from the server
317
- # and parses them. It also checks for errors and raises exceptions with the appropriate messages.
318
- def parse_response(response, raw=false) # :nodoc:
319
- if response.kind_of?(Net::HTTPServerError)
320
- raise DropboxError.new("Dropbox Server Error: #{response} - #{response.body}", response)
321
- elsif response.kind_of?(Net::HTTPUnauthorized)
322
- raise DropboxAuthError.new(response, "User is not authenticated.")
323
- elsif not response.kind_of?(Net::HTTPSuccess)
324
- begin
325
- d = JSON.parse(response.body)
326
- rescue
327
- raise DropboxError.new("Dropbox Server Error: body=#{response.body}", response)
328
- end
329
- if d['user_error'] and d['error']
330
- raise DropboxError.new(d['error'], response, d['user_error']) #user_error is translated
331
- elsif d['error']
332
- raise DropboxError.new(d['error'], response)
333
- else
334
- raise DropboxError.new(response.body, response)
335
- end
336
- end
337
-
338
- return response.body if raw
339
-
340
- begin
341
- return JSON.parse(response.body)
342
- rescue JSON::ParserError
343
- raise DropboxError.new("Unable to parse JSON response: #{response.body}", response)
344
- end
345
676
  end
346
677
 
347
- # Returns account info in a Hash object
678
+ # Returns some information about the current user's Dropbox account (the "current user"
679
+ # is the user associated with the access token you're using).
348
680
  #
349
681
  # For a detailed description of what this call returns, visit:
350
682
  # https://www.dropbox.com/developers/reference/api#account-info
351
683
  def account_info()
352
684
  response = @session.do_get build_url("/account/info")
353
- parse_response(response)
685
+ Dropbox::parse_response(response)
354
686
  end
355
687
 
356
688
  # Uploads a file to a server. This uses the HTTP PUT upload method for simplicity
357
689
  #
358
- # Arguments:
359
- # * to_path: The directory path to upload the file to. If the destination
690
+ # Args:
691
+ # * +to_path+: The directory path to upload the file to. If the destination
360
692
  # directory does not yet exist, it will be created.
361
- # * file_obj: A file-like object to upload. If you would like, you can
693
+ # * +file_obj+: A file-like object to upload. If you would like, you can
362
694
  # pass a string as file_obj.
363
- # * overwrite: Whether to overwrite an existing file at the given path. [default is False]
695
+ # * +overwrite+: Whether to overwrite an existing file at the given path. [default is False]
364
696
  # If overwrite is False and a file already exists there, Dropbox
365
697
  # will rename the upload to make sure it doesn't overwrite anything.
366
698
  # You must check the returned metadata to know what this new name is.
367
699
  # This field should only be True if your intent is to potentially
368
700
  # clobber changes to a file that you don't know about.
369
- # * parent_rev: The rev field from the 'parent' of this upload. [optional]
701
+ # * +parent_rev+: The rev field from the 'parent' of this upload. [optional]
370
702
  # If your intent is to update the file at the given path, you should
371
703
  # pass the parent_rev parameter set to the rev value from the most recent
372
704
  # metadata you have of the existing file at that path. If the server
@@ -376,10 +708,11 @@ class DropboxClient
376
708
  # The file will always be overwritten if you send the most-recent parent_rev,
377
709
  # and it will never be overwritten you send a less-recent one.
378
710
  # Returns:
379
- # * a Hash containing the metadata of the newly uploaded file. The file may have a different name if it conflicted.
711
+ # * a Hash containing the metadata of the newly uploaded file. The file may have a different
712
+ # name if it conflicted.
380
713
  #
381
714
  # Simple Example
382
- # client = DropboxClient(session, :app_folder)
715
+ # client = DropboxClient(oauth2_access_token)
383
716
  # #session is a DropboxSession I've already authorized
384
717
  # client.put_file('/test_file_on_dropbox', open('/tmp/test_file'))
385
718
  # This will upload the "/tmp/test_file" from my computer into the root of my App's app folder
@@ -398,14 +731,14 @@ class DropboxClient
398
731
  {"Content-Type" => "application/octet-stream"},
399
732
  file_obj)
400
733
 
401
- parse_response(response)
734
+ Dropbox::parse_response(response)
402
735
  end
403
736
 
404
737
  # Returns a ChunkedUploader object.
405
738
  #
406
739
  # Args:
407
- # * file_obj: The file-like object to be uploaded. Must support .read()
408
- # * total_size: The total size of file_obj
740
+ # * +file_obj+: The file-like object to be uploaded. Must support .read()
741
+ # * +total_size+: The total size of file_obj
409
742
  def get_chunked_uploader(file_obj, total_size)
410
743
  ChunkedUploader.new(self, file_obj, total_size)
411
744
  end
@@ -428,7 +761,7 @@ class DropboxClient
428
761
  # be called again to resume the upload.
429
762
  #
430
763
  # Args:
431
- # * chunk_size: The chunk size for each individual upload. Defaults to 4MB.
764
+ # * +chunk_size+: The chunk size for each individual upload. Defaults to 4MB.
432
765
  def upload(chunk_size=4*1024*1024)
433
766
  last_chunk = nil
434
767
 
@@ -439,7 +772,7 @@ class DropboxClient
439
772
 
440
773
  resp = {}
441
774
  begin
442
- resp = @client.parse_response(@client.partial_chunked_upload(last_chunk, @upload_id, @offset))
775
+ resp = Dropbox::parse_response(@client.partial_chunked_upload(last_chunk, @upload_id, @offset))
443
776
  last_chunk = nil
444
777
  rescue DropboxError => e
445
778
  resp = JSON.parse(e.http_response.body)
@@ -457,9 +790,9 @@ class DropboxClient
457
790
  # Completes a file upload
458
791
  #
459
792
  # Args:
460
- # * to_path: The directory path to upload the file to. If the destination
793
+ # * +to_path+: The directory path to upload the file to. If the destination
461
794
  # directory does not yet exist, it will be created.
462
- # * overwrite: Whether to overwrite an existing file at the given path. [default is False]
795
+ # * +overwrite+: Whether to overwrite an existing file at the given path. [default is False]
463
796
  # If overwrite is False and a file already exists there, Dropbox
464
797
  # will rename the upload to make sure it doesn't overwrite anything.
465
798
  # You must check the returned metadata to know what this new name is.
@@ -481,7 +814,7 @@ class DropboxClient
481
814
  # https://www.dropbox.com/developers/reference/api#metadata
482
815
  def finish(to_path, overwrite=false, parent_rev=nil)
483
816
  response = @client.commit_chunked_upload(to_path, @upload_id, overwrite, parent_rev)
484
- @client.parse_response(response)
817
+ Dropbox::parse_response(response)
485
818
  end
486
819
  end
487
820
 
@@ -505,28 +838,28 @@ class DropboxClient
505
838
  # Download a file
506
839
  #
507
840
  # Args:
508
- # * from_path: The path to the file to be downloaded
509
- # * rev: A previous revision value of the file to be downloaded
841
+ # * +from_path+: The path to the file to be downloaded
842
+ # * +rev+: A previous revision value of the file to be downloaded
510
843
  #
511
844
  # Returns:
512
845
  # * The file contents.
513
846
  def get_file(from_path, rev=nil)
514
847
  response = get_file_impl(from_path, rev)
515
- parse_response(response, raw=true)
848
+ Dropbox::parse_response(response, raw=true)
516
849
  end
517
850
 
518
851
  # Download a file and get its metadata.
519
852
  #
520
853
  # Args:
521
- # * from_path: The path to the file to be downloaded
522
- # * rev: A previous revision value of the file to be downloaded
854
+ # * +from_path+: The path to the file to be downloaded
855
+ # * +rev+: A previous revision value of the file to be downloaded
523
856
  #
524
857
  # Returns:
525
858
  # * The file contents.
526
859
  # * The file metadata as a hash.
527
860
  def get_file_and_metadata(from_path, rev=nil)
528
861
  response = get_file_impl(from_path, rev)
529
- parsed_response = parse_response(response, raw=true)
862
+ parsed_response = Dropbox::parse_response(response, raw=true)
530
863
  metadata = parse_metadata(response)
531
864
  return parsed_response, metadata
532
865
  end
@@ -534,8 +867,8 @@ class DropboxClient
534
867
  # Download a file (helper method - don't call this directly).
535
868
  #
536
869
  # Args:
537
- # * from_path: The path to the file to be downloaded
538
- # * rev: A previous revision value of the file to be downloaded
870
+ # * +from_path+: The path to the file to be downloaded
871
+ # * +rev+: A previous revision value of the file to be downloaded
539
872
  #
540
873
  # Returns:
541
874
  # * The HTTPResponse for the file download request.
@@ -551,7 +884,7 @@ class DropboxClient
551
884
  # Parses out file metadata from a raw dropbox HTTP response.
552
885
  #
553
886
  # Args:
554
- # * dropbox_raw_response: The raw, unparsed HTTPResponse from Dropbox.
887
+ # * +dropbox_raw_response+: The raw, unparsed HTTPResponse from Dropbox.
555
888
  #
556
889
  # Returns:
557
890
  # * The metadata of the file as a hash.
@@ -569,9 +902,9 @@ class DropboxClient
569
902
 
570
903
  # Copy a file or folder to a new location.
571
904
  #
572
- # Arguments:
573
- # * from_path: The path to the file or folder to be copied.
574
- # * to_path: The destination path of the file or folder to be copied.
905
+ # Args:
906
+ # * +from_path+: The path to the file or folder to be copied.
907
+ # * +to_path+: The destination path of the file or folder to be copied.
575
908
  # This parameter should include the destination filename (e.g.
576
909
  # from_path: '/test.txt', to_path: '/dir/test.txt'). If there's
577
910
  # already a file at the to_path, this copy will be renamed to
@@ -588,13 +921,13 @@ class DropboxClient
588
921
  "to_path" => format_path(to_path, false),
589
922
  }
590
923
  response = @session.do_post build_url("/fileops/copy", params)
591
- parse_response(response)
924
+ Dropbox::parse_response(response)
592
925
  end
593
926
 
594
927
  # Create a folder.
595
928
  #
596
929
  # Arguments:
597
- # * path: The path of the new folder.
930
+ # * +path+: The path of the new folder.
598
931
  #
599
932
  # Returns:
600
933
  # * A hash with the metadata of the newly created folder.
@@ -607,13 +940,13 @@ class DropboxClient
607
940
  }
608
941
  response = @session.do_post build_url("/fileops/create_folder", params)
609
942
 
610
- parse_response(response)
943
+ Dropbox::parse_response(response)
611
944
  end
612
945
 
613
946
  # Deletes a file
614
947
  #
615
948
  # Arguments:
616
- # * path: The path of the file to delete
949
+ # * +path+: The path of the file to delete
617
950
  #
618
951
  # Returns:
619
952
  # * A Hash with the metadata of file just deleted.
@@ -625,14 +958,14 @@ class DropboxClient
625
958
  "path" => format_path(path, false),
626
959
  }
627
960
  response = @session.do_post build_url("/fileops/delete", params)
628
- parse_response(response)
961
+ Dropbox::parse_response(response)
629
962
  end
630
963
 
631
964
  # Moves a file
632
965
  #
633
966
  # Arguments:
634
- # * from_path: The path of the file to be moved
635
- # * to_path: The destination path of the file or folder to be moved
967
+ # * +from_path+: The path of the file to be moved
968
+ # * +to_path+: The destination path of the file or folder to be moved
636
969
  # If the file or folder already exists, it will be renamed to be unique.
637
970
  #
638
971
  # Returns:
@@ -646,7 +979,7 @@ class DropboxClient
646
979
  "to_path" => format_path(to_path, false),
647
980
  }
648
981
  response = @session.do_post build_url("/fileops/move", params)
649
- parse_response(response)
982
+ Dropbox::parse_response(response)
650
983
  end
651
984
 
652
985
  # Retrives metadata for a file or folder
@@ -686,7 +1019,7 @@ class DropboxClient
686
1019
  if response.kind_of? Net::HTTPRedirection
687
1020
  raise DropboxNotModified.new("metadata not modified")
688
1021
  end
689
- parse_response(response)
1022
+ Dropbox::parse_response(response)
690
1023
  end
691
1024
 
692
1025
  # Search directory for filenames matching query
@@ -711,7 +1044,7 @@ class DropboxClient
711
1044
  }
712
1045
 
713
1046
  response = @session.do_get build_url("/search/#{@root}#{format_path(path)}", params)
714
- parse_response(response)
1047
+ Dropbox::parse_response(response)
715
1048
  end
716
1049
 
717
1050
  # Retrive revisions of a file
@@ -734,7 +1067,7 @@ class DropboxClient
734
1067
  }
735
1068
 
736
1069
  response = @session.do_get build_url("/revisions/#{@root}#{format_path(path)}", params)
737
- parse_response(response)
1070
+ Dropbox::parse_response(response)
738
1071
 
739
1072
  end
740
1073
 
@@ -754,7 +1087,7 @@ class DropboxClient
754
1087
  }
755
1088
 
756
1089
  response = @session.do_post build_url("/restore/#{@root}#{format_path(path)}", params)
757
- parse_response(response)
1090
+ Dropbox::parse_response(response)
758
1091
  end
759
1092
 
760
1093
  # Returns a direct link to a media file
@@ -771,7 +1104,7 @@ class DropboxClient
771
1104
  # {'url': 'https://dl.dropbox.com/0/view/wvxv1fw6on24qw7/file.mov', 'expires': 'Thu, 16 Sep 2011 01:01:25 +0000'}
772
1105
  def media(path)
773
1106
  response = @session.do_get build_url("/media/#{@root}#{format_path(path)}")
774
- parse_response(response)
1107
+ Dropbox::parse_response(response)
775
1108
  end
776
1109
 
777
1110
  # Get a URL to share a media file
@@ -790,7 +1123,7 @@ class DropboxClient
790
1123
  # https://www.dropbox.com/developers/reference/api#shares
791
1124
  def shares(path)
792
1125
  response = @session.do_get build_url("/shares/#{@root}#{format_path(path)}")
793
- parse_response(response)
1126
+ Dropbox::parse_response(response)
794
1127
  end
795
1128
 
796
1129
  # Download a thumbnail for an image.
@@ -806,7 +1139,7 @@ class DropboxClient
806
1139
  # * The thumbnail data
807
1140
  def thumbnail(from_path, size='large')
808
1141
  response = thumbnail_impl(from_path, size)
809
- parse_response(response, raw=true)
1142
+ Dropbox::parse_response(response, raw=true)
810
1143
  end
811
1144
 
812
1145
  # Download a thumbnail for an image along with the image's metadata.
@@ -820,7 +1153,7 @@ class DropboxClient
820
1153
  # * The metadata for the image as a hash
821
1154
  def thumbnail_and_metadata(from_path, size='large')
822
1155
  response = thumbnail_impl(from_path, size)
823
- parsed_response = parse_response(response, raw=true)
1156
+ parsed_response = Dropbox::parse_response(response, raw=true)
824
1157
  metadata = parse_metadata(response)
825
1158
  return parsed_response, metadata
826
1159
  end
@@ -876,14 +1209,14 @@ class DropboxClient
876
1209
  end
877
1210
 
878
1211
  response = @session.do_post build_url("/delta", params)
879
- parse_response(response)
1212
+ Dropbox::parse_response(response)
880
1213
  end
881
1214
 
882
1215
  # Download a thumbnail (helper method - don't call this directly).
883
1216
  #
884
1217
  # Args:
885
- # * from_path: The path to the file to be thumbnailed.
886
- # * size: A string describing the desired thumbnail size. See thumbnail()
1218
+ # * +from_path+: The path to the file to be thumbnailed.
1219
+ # * +size+: A string describing the desired thumbnail size. See thumbnail()
887
1220
  # for details.
888
1221
  #
889
1222
  # Returns:
@@ -906,7 +1239,7 @@ class DropboxClient
906
1239
  # used to instantly copy that file to the Dropbox of another account.
907
1240
  #
908
1241
  # Args:
909
- # * path: The path to the file for a copy ref to be created on.
1242
+ # * +path+: The path to the file for a copy ref to be created on.
910
1243
  #
911
1244
  # Returns:
912
1245
  # * A Hash object that looks like the following example:
@@ -916,15 +1249,15 @@ class DropboxClient
916
1249
 
917
1250
  response = @session.do_get(build_url(path, {}))
918
1251
 
919
- parse_response(response)
1252
+ Dropbox::parse_response(response)
920
1253
  end
921
1254
 
922
1255
  # Adds the file referenced by the copy ref to the specified path
923
1256
  #
924
1257
  # Args:
925
- # * copy_ref: A copy ref string that was returned from a create_copy_ref call.
1258
+ # * +copy_ref+: A copy ref string that was returned from a create_copy_ref call.
926
1259
  # The copy_ref can be created from any other Dropbox account, or from the same account.
927
- # * to_path: The path to where the file will be created.
1260
+ # * +to_path+: The path to where the file will be created.
928
1261
  #
929
1262
  # Returns:
930
1263
  # * A hash with the metadata of the new file.
@@ -937,7 +1270,7 @@ class DropboxClient
937
1270
 
938
1271
  response = @session.do_post(build_url(path, params))
939
1272
 
940
- parse_response(response)
1273
+ Dropbox::parse_response(response)
941
1274
  end
942
1275
 
943
1276
  def build_url(url, params=nil, content_server=false) # :nodoc:
@@ -963,7 +1296,7 @@ class DropboxClient
963
1296
  end
964
1297
 
965
1298
  #From the oauth spec plus "/". Slash should not be ecsaped
966
- RESERVED_CHARACTERS = /[^a-zA-Z0-9\-\.\_\~\/]/
1299
+ RESERVED_CHARACTERS = /[^a-zA-Z0-9\-\.\_\~\/]/ # :nodoc:
967
1300
 
968
1301
  def format_path(path, escape=true) # :nodoc:
969
1302
  path = path.gsub(/\/+/,"/")