dropbox-sdk-forked_v2 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,1484 @@
1
+ require 'uri'
2
+ require 'net/https'
3
+ require 'cgi'
4
+ require 'json'
5
+ require 'yaml'
6
+ require 'base64'
7
+ require 'securerandom'
8
+ require 'pp'
9
+
10
+ module Dropbox # :nodoc:
11
+ API_SERVER = "api.dropboxapi.com"
12
+ API_CONTENT_SERVER = "content.dropboxapi.com"
13
+ API_NOTIFY_SERVER = "notify.dropboxapi.com"
14
+ WEB_SERVER = "www.dropbox.com"
15
+
16
+ SERVERS = {
17
+ :api => API_SERVER,
18
+ :content => API_CONTENT_SERVER,
19
+ :notify => API_NOTIFY_SERVER,
20
+ :web => WEB_SERVER
21
+ }
22
+
23
+ API_VERSION = 2
24
+ SDK_VERSION = "1.6.5-v2-fork"
25
+
26
+ TRUSTED_CERT_FILE = File.join(File.dirname(__FILE__), 'trusted-certs.crt')
27
+
28
+ def self.clean_params(params)
29
+ r = {}
30
+ params.each do |k, v|
31
+ r[k] = v.to_s if not v.nil?
32
+ end
33
+ r
34
+ end
35
+
36
+ def self.make_query_string(params)
37
+ clean_params(params).collect {|k, v|
38
+ CGI.escape(k) + "=" + CGI.escape(v)
39
+ }.join("&")
40
+ end
41
+
42
+ def self.do_http(uri, request) # :nodoc:
43
+
44
+ http = Net::HTTP.new(uri.host, uri.port)
45
+
46
+ http.use_ssl = true
47
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
48
+ http.ca_file = Dropbox::TRUSTED_CERT_FILE
49
+ http.read_timeout = 600
50
+
51
+ if RUBY_VERSION >= '1.9'
52
+ # SSL protocol and ciphersuite settings are supported strating with version 1.9
53
+ http.ssl_version = 'TLSv1'
54
+ http.ciphers = 'ECDHE-RSA-AES256-GCM-SHA384:'\
55
+ 'ECDHE-RSA-AES256-SHA384:'\
56
+ 'ECDHE-RSA-AES256-SHA:'\
57
+ 'ECDHE-RSA-AES128-GCM-SHA256:'\
58
+ 'ECDHE-RSA-AES128-SHA256:'\
59
+ 'ECDHE-RSA-AES128-SHA:'\
60
+ 'ECDHE-RSA-RC4-SHA:'\
61
+ 'DHE-RSA-AES256-GCM-SHA384:'\
62
+ 'DHE-RSA-AES256-SHA256:'\
63
+ 'DHE-RSA-AES256-SHA:'\
64
+ 'DHE-RSA-AES128-GCM-SHA256:'\
65
+ 'DHE-RSA-AES128-SHA256:'\
66
+ 'DHE-RSA-AES128-SHA:'\
67
+ 'AES256-GCM-SHA384:'\
68
+ 'AES256-SHA256:'\
69
+ 'AES256-SHA:'\
70
+ 'AES128-GCM-SHA256:'\
71
+ 'AES128-SHA256:'\
72
+ 'AES128-SHA'
73
+ end
74
+
75
+ # Important security note!
76
+ # Some Ruby versions (e.g. the one that ships with OS X) do not raise
77
+ # an exception if certificate validation fails. We therefore have to
78
+ # add a custom callback to ensure that invalid certs are not accepted.
79
+ # Some specific error codes are let through, so we change the error
80
+ # code to make sure that Ruby throws an exception if certificate
81
+ # validation fails.
82
+ #
83
+ # See the man page for 'verify' for more information on error codes.
84
+ #
85
+ # You can comment out this code if your Ruby version is not vulnerable.
86
+ http.verify_callback = proc do |preverify_ok, ssl_context|
87
+ # 0 is the error code for success
88
+ if preverify_ok && ssl_context.error == 0
89
+ true
90
+ else
91
+ # 7 is the error code for certification signature failure
92
+ ssl_context.error = 7
93
+ false
94
+ end
95
+ end
96
+
97
+ #We use this to better understand how developers are using our SDKs.
98
+ request['User-Agent'] = "OfficialDropboxRubySDK/#{Dropbox::SDK_VERSION}"
99
+
100
+ begin
101
+ http.request(request)
102
+ rescue OpenSSL::SSL::SSLError => e
103
+ raise DropboxError.new("SSL error connecting to Dropbox. " +
104
+ "There may be a problem with the set of certificates in \"#{Dropbox::TRUSTED_CERT_FILE}\". #{e.message}")
105
+ end
106
+ end
107
+
108
+ # Parse response. You probably shouldn't be calling this directly. This takes responses from the server
109
+ # and parses them. It also checks for errors and raises exceptions with the appropriate messages.
110
+ def self.parse_response(response, raw=false) # :nodoc:
111
+ if response.is_a?(Net::HTTPServerError)
112
+ raise DropboxError.new("Dropbox Server Error: #{response} - #{response.body}", response)
113
+ elsif response.is_a?(Net::HTTPUnauthorized)
114
+ raise DropboxAuthError.new("User is not authenticated.", response)
115
+ elsif !response.is_a?(Net::HTTPSuccess)
116
+ begin
117
+ d = JSON.parse(response.body)
118
+ rescue
119
+ raise DropboxError.new("Dropbox Server Error: body=#{response.body}", response)
120
+ end
121
+ if d['user_error'] and d['error']
122
+ raise DropboxError.new(d['error'], response, d['user_error']) #user_error is translated
123
+ elsif d['error']
124
+ raise DropboxError.new(d['error'], response)
125
+ else
126
+ raise DropboxError.new(response.body, response)
127
+ end
128
+ end
129
+
130
+ return response.body if raw
131
+
132
+ begin
133
+ return JSON.parse(response.body)
134
+ rescue JSON::ParserError
135
+ raise DropboxError.new("Unable to parse JSON response: #{response.body}", response)
136
+ end
137
+ end
138
+
139
+ # A string comparison function that is resistant to timing attacks. The time it takes to
140
+ # run will leak the length of the secret string, but not any of the character values.
141
+ def self.safe_string_equals(a, b)
142
+ if a.length != b.length
143
+ false
144
+ else
145
+ a.chars.zip(b.chars).map {|ac,bc| ac == bc}.reduce(true, :&)
146
+ end
147
+ end
148
+ end
149
+
150
+ class DropboxSessionBase # :nodoc:
151
+
152
+ attr_writer :locale
153
+
154
+ def initialize(locale)
155
+ @locale = locale
156
+ end
157
+
158
+ private
159
+
160
+ def build_url(path, server)
161
+ port = 443
162
+ host = Dropbox::SERVERS[server]
163
+ full_path = "/#{Dropbox::API_VERSION}#{path}"
164
+ return URI::HTTPS.build({:host => host, :path => full_path})
165
+ end
166
+
167
+ def build_url_with_params(path, params, server) # :nodoc:
168
+ target = build_url(path, server)
169
+ params['locale'] = @locale
170
+ target.query = Dropbox::make_query_string(params)
171
+ return target
172
+ end
173
+
174
+ protected
175
+
176
+ def do_http(uri, request) # :nodoc:
177
+ sign_request(request)
178
+ Dropbox::do_http(uri, request)
179
+ end
180
+
181
+ public
182
+
183
+ def do_get(path, params=nil, server=:api) # :nodoc:
184
+ params ||= {}
185
+ assert_authorized
186
+ uri = build_url_with_params(path, params, server)
187
+ do_http(uri, Net::HTTP::Get.new(uri.request_uri))
188
+ end
189
+
190
+ def do_http_with_body(uri, request, body)
191
+ if body != nil
192
+ if body.is_a?(Hash)
193
+ # I don't understand why set_form_data wipes the content_type val
194
+ content_type = request.content_type
195
+ request.set_form_data(Dropbox::clean_params(body))
196
+ request.content_type = (content_type.nil?) ? "" : content_type
197
+ elsif body.respond_to?(:read)
198
+ if body.respond_to?(:length)
199
+ request["Content-Length"] = body.length.to_s
200
+ elsif body.respond_to?(:stat) && body.stat.respond_to?(:size)
201
+ request["Content-Length"] = body.stat.size.to_s
202
+ else
203
+ raise ArgumentError, "Don't know how to handle 'body' (responds to 'read' but not to 'length' or 'stat.size')."
204
+ end
205
+ request.body_stream = body
206
+ else
207
+ s = body.to_s
208
+ request["Content-Length"] = s.length
209
+ request.body = s
210
+ end
211
+ end
212
+ do_http(uri, request)
213
+ end
214
+
215
+ def do_post(path, params=nil, headers=nil, server=:api) # :nodoc:
216
+ params ||= {}
217
+ assert_authorized
218
+ uri = build_url(path, server)
219
+ do_http_with_body(uri, Net::HTTP::Post.new(uri.request_uri, headers), params)
220
+ end
221
+
222
+ def do_put(path, params=nil, headers=nil, body=nil, server=:api) # :nodoc:
223
+ params ||= {}
224
+ assert_authorized
225
+ uri = build_url_with_params(path, params, server)
226
+ do_http_with_body(uri, Net::HTTP::Put.new(uri.request_uri, headers), body)
227
+ end
228
+ end
229
+
230
+ # DropboxSession is responsible for holding OAuth 1 information. It knows how to take your consumer key and secret
231
+ # and request an access token, an authorize url, and get an access token. You just need to pass it to
232
+ # DropboxClient after its been authorized.
233
+ class DropboxSession < DropboxSessionBase # :nodoc:
234
+
235
+ # * consumer_key - Your Dropbox application's "app key".
236
+ # * consumer_secret - Your Dropbox application's "app secret".
237
+ def initialize(consumer_key, consumer_secret, locale=nil)
238
+ super(locale)
239
+ @consumer_key = consumer_key
240
+ @consumer_secret = consumer_secret
241
+ @request_token = nil
242
+ @access_token = nil
243
+ end
244
+
245
+ private
246
+
247
+ def build_auth_header(token) # :nodoc:
248
+ header = "OAuth oauth_version=\"1.0\", oauth_signature_method=\"PLAINTEXT\", " +
249
+ "oauth_consumer_key=\"#{URI.escape(@consumer_key)}\", "
250
+ if token
251
+ key = URI.escape(token.key)
252
+ secret = URI.escape(token.secret)
253
+ header += "oauth_token=\"#{key}\", oauth_signature=\"#{URI.escape(@consumer_secret)}&#{secret}\""
254
+ else
255
+ header += "oauth_signature=\"#{URI.escape(@consumer_secret)}&\""
256
+ end
257
+ header
258
+ end
259
+
260
+ def do_get_with_token(url, token) # :nodoc:
261
+ uri = URI.parse(url)
262
+ request = Net::HTTP::Get.new(uri.request_uri)
263
+ request.add_field('Authorization', build_auth_header(token))
264
+ Dropbox::do_http(uri, request)
265
+ end
266
+
267
+ protected
268
+
269
+ def sign_request(request) # :nodoc:
270
+ request.add_field('Authorization', build_auth_header(@access_token))
271
+ end
272
+
273
+ public
274
+
275
+ def get_token(url_end, input_token, error_message_prefix) #: nodoc:
276
+ response = do_get_with_token("https://#{Dropbox::API_SERVER}:443/#{Dropbox::API_VERSION}/oauth#{url_end}", input_token)
277
+ if not response.kind_of?(Net::HTTPSuccess) # it must be a 200
278
+ raise DropboxAuthError.new("#{error_message_prefix} Server returned #{response.code}: #{response.message}.", response)
279
+ end
280
+ parts = CGI.parse(response.body)
281
+
282
+ if !parts.has_key? "oauth_token" and parts["oauth_token"].length != 1
283
+ raise DropboxAuthError.new("Invalid response from #{url_end}: missing \"oauth_token\" parameter: #{response.body}", response)
284
+ end
285
+ if !parts.has_key? "oauth_token_secret" and parts["oauth_token_secret"].length != 1
286
+ raise DropboxAuthError.new("Invalid response from #{url_end}: missing \"oauth_token\" parameter: #{response.body}", response)
287
+ end
288
+
289
+ OAuthToken.new(parts["oauth_token"][0], parts["oauth_token_secret"][0])
290
+ end
291
+
292
+ # This returns a request token. Requests one from the dropbox server using the provided application key and secret if nessecary.
293
+ def get_request_token()
294
+ @request_token ||= get_token("/request_token", nil, "Error getting request token. Is your app key and secret correctly set?")
295
+ end
296
+
297
+ # This returns a URL that your user must visit to grant
298
+ # permissions to this application.
299
+ def get_authorize_url(callback=nil)
300
+ get_request_token()
301
+
302
+ url = "/#{Dropbox::API_VERSION}/oauth/authorize?oauth_token=#{URI.escape(@request_token.key)}"
303
+ if callback
304
+ url += "&oauth_callback=#{URI.escape(callback)}"
305
+ end
306
+ if @locale
307
+ url += "&locale=#{URI.escape(@locale)}"
308
+ end
309
+
310
+ "https://#{Dropbox::WEB_SERVER}#{url}"
311
+ end
312
+
313
+ # Clears the access_token
314
+ def clear_access_token
315
+ @access_token = nil
316
+ end
317
+
318
+ # Returns the request token, or nil if one hasn't been acquired yet.
319
+ def request_token
320
+ @request_token
321
+ end
322
+
323
+ # Returns the access token, or nil if one hasn't been acquired yet.
324
+ def access_token
325
+ @access_token
326
+ end
327
+
328
+ # Given a saved request token and secret, set this location's token and secret
329
+ # * token - this is the request token
330
+ # * secret - this is the request token secret
331
+ def set_request_token(key, secret)
332
+ @request_token = OAuthToken.new(key, secret)
333
+ end
334
+
335
+ # Given a saved access token and secret, you set this Session to use that token and secret
336
+ # * token - this is the access token
337
+ # * secret - this is the access token secret
338
+ def set_access_token(key, secret)
339
+ @access_token = OAuthToken.new(key, secret)
340
+ end
341
+
342
+ # Returns the access token. If this DropboxSession doesn't yet have an access_token, it requests one
343
+ # using the request_token generate from your app's token and secret. This request will fail unless
344
+ # your user has gone to the authorize_url and approved your request
345
+ def get_access_token
346
+ return @access_token if authorized?
347
+
348
+ if @request_token.nil?
349
+ raise RuntimeError.new("No request token. You must set this or get an authorize url first.")
350
+ end
351
+
352
+ @access_token = get_token("/access_token", @request_token, "Couldn't get access token.")
353
+ end
354
+
355
+ # If we have an access token, then do nothing. If not, throw a RuntimeError.
356
+ def assert_authorized
357
+ unless authorized?
358
+ raise RuntimeError.new('Session does not yet have a request token')
359
+ end
360
+ end
361
+
362
+ # Returns true if this Session has been authorized and has an access_token.
363
+ def authorized?
364
+ !!@access_token
365
+ end
366
+
367
+ # serialize the DropboxSession.
368
+ # At DropboxSession's state is capture in three key/secret pairs. Consumer, request, and access.
369
+ # Serialize returns these in a YAML string, generated from a converted array of the form:
370
+ # [consumer_key, consumer_secret, request_token.token, request_token.secret, access_token.token, access_token.secret]
371
+ # access_token is only included if it already exists in the DropboxSesssion
372
+ def serialize
373
+ toreturn = []
374
+ if @access_token
375
+ toreturn.push @access_token.secret, @access_token.key
376
+ end
377
+
378
+ get_request_token
379
+
380
+ toreturn.push @request_token.secret, @request_token.key
381
+ toreturn.push @consumer_secret, @consumer_key
382
+
383
+ toreturn.to_yaml
384
+ end
385
+
386
+ # Takes a serialized DropboxSession YAML String and returns a new DropboxSession object
387
+ def self.deserialize(ser)
388
+ ser = YAML::load(ser)
389
+ session = DropboxSession.new(ser.pop, ser.pop)
390
+ session.set_request_token(ser.pop, ser.pop)
391
+
392
+ if ser.length > 0
393
+ session.set_access_token(ser.pop, ser.pop)
394
+ end
395
+ session
396
+ end
397
+ end
398
+
399
+
400
+ class DropboxOAuth2Session < DropboxSessionBase # :nodoc:
401
+
402
+ def initialize(oauth2_access_token, locale=nil)
403
+ super(locale)
404
+ if not oauth2_access_token.is_a?(String)
405
+ raise "bad type for oauth2_access_token (expecting String)"
406
+ end
407
+ @access_token = oauth2_access_token
408
+ end
409
+
410
+ def assert_authorized
411
+ true
412
+ end
413
+
414
+ protected
415
+
416
+ def sign_request(request) # :nodoc:
417
+ request.add_field('Authorization', 'Bearer ' + @access_token)
418
+ end
419
+ end
420
+
421
+ # Base class for the two OAuth 2 authorization helpers.
422
+ class DropboxOAuth2FlowBase # :nodoc:
423
+ def initialize(consumer_key, consumer_secret, locale=nil)
424
+ if not consumer_key.is_a?(String)
425
+ raise ArgumentError, "consumer_key must be a String, got #{consumer_key.inspect}"
426
+ end
427
+ if not consumer_secret.is_a?(String)
428
+ raise ArgumentError, "consumer_secret must be a String, got #{consumer_secret.inspect}"
429
+ end
430
+ if not (locale.nil? or locale.is_a?(String))
431
+ raise ArgumentError, "locale must be a String or nil, got #{locale.inspect}"
432
+ end
433
+ @consumer_key = consumer_key
434
+ @consumer_secret = consumer_secret
435
+ @locale = locale
436
+ end
437
+
438
+ def _get_authorize_url(redirect_uri, state)
439
+ params = {
440
+ "client_id" => @consumer_key,
441
+ "response_type" => "code",
442
+ "redirect_uri" => redirect_uri,
443
+ "state" => state,
444
+ "locale" => @locale,
445
+ }
446
+
447
+ host = Dropbox::WEB_SERVER
448
+ path = "/#{Dropbox::API_VERSION}/oauth2/authorize"
449
+
450
+ target = URI::Generic.new("https", nil, host, nil, nil, path, nil, nil, nil)
451
+ target.query = Dropbox::make_query_string(params)
452
+
453
+ target.to_s
454
+ end
455
+
456
+ # Finish the OAuth 2 authorization process. If you used a redirect_uri, pass that in.
457
+ # Will return an access token string that you can use with DropboxClient.
458
+ def _finish(code, original_redirect_uri)
459
+ if not code.is_a?(String)
460
+ raise ArgumentError, "code must be a String"
461
+ end
462
+
463
+ uri = URI.parse("https://#{Dropbox::API_SERVER}/1/oauth2/token")
464
+ request = Net::HTTP::Post.new(uri.request_uri)
465
+ client_credentials = @consumer_key + ':' + @consumer_secret
466
+ request.add_field('Authorization', 'Basic ' + Base64.encode64(client_credentials).chomp("\n"))
467
+
468
+ params = {
469
+ "grant_type" => "authorization_code",
470
+ "code" => code,
471
+ "redirect_uri" => original_redirect_uri,
472
+ "locale" => @locale,
473
+ }
474
+
475
+ request.set_form_data(Dropbox::clean_params(params))
476
+
477
+ response = Dropbox::do_http(uri, request)
478
+
479
+ j = Dropbox::parse_response(response)
480
+ ["token_type", "access_token", "uid"].each { |k|
481
+ if not j.has_key?(k)
482
+ raise DropboxError.new("Bad response from /token: missing \"#{k}\".")
483
+ end
484
+ if not j[k].is_a?(String)
485
+ raise DropboxError.new("Bad response from /token: field \"#{k}\" is not a string.")
486
+ end
487
+ }
488
+ if j["token_type"] != "bearer" and j["token_type"] != "Bearer"
489
+ raise DropboxError.new("Bad response from /token: \"token_type\" is \"#{ j['token_type'] }\".")
490
+ end
491
+
492
+ return j['access_token'], j['uid']
493
+ end
494
+ end
495
+
496
+ # OAuth 2 authorization helper for apps that can't provide a redirect URI
497
+ # (such as the command line example apps).
498
+ class DropboxOAuth2FlowNoRedirect < DropboxOAuth2FlowBase
499
+
500
+ # * consumer_key: Your Dropbox API app's "app key"
501
+ # * consumer_secret: Your Dropbox API app's "app secret"
502
+ # * locale: The locale of the user currently using your app.
503
+ def initialize(consumer_key, consumer_secret, locale=nil)
504
+ super(consumer_key, consumer_secret, locale)
505
+ end
506
+
507
+ # Returns a authorization_url, which is a page on Dropbox's website. Have the user
508
+ # visit this URL and approve your app.
509
+ def start()
510
+ _get_authorize_url(nil, nil)
511
+ end
512
+
513
+ # If the user approves your app, they will be presented with an "authorization code".
514
+ # Have the user copy/paste that authorization code into your app and then call this
515
+ # method to get an access token.
516
+ #
517
+ # Returns a two-entry list (access_token, user_id)
518
+ # * access_token is an access token string that can be passed to DropboxClient.
519
+ # * user_id is the Dropbox user ID of the user that just approved your app.
520
+ def finish(code)
521
+ _finish(code, nil)
522
+ end
523
+ end
524
+
525
+ # The standard OAuth 2 authorization helper. Use this if you're writing a web app.
526
+ class DropboxOAuth2Flow < DropboxOAuth2FlowBase
527
+
528
+ # * consumer_key: Your Dropbox API app's "app key"
529
+ # * consumer_secret: Your Dropbox API app's "app secret"
530
+ # * redirect_uri: The URI that the Dropbox server will redirect the user to after the user
531
+ # finishes authorizing your app. This URI must be HTTPs-based and pre-registered with
532
+ # the Dropbox servers, though localhost URIs are allowed without pre-registration and can
533
+ # be either HTTP or HTTPS.
534
+ # * session: A hash that represents the current web app session (will be used to save the CSRF
535
+ # token)
536
+ # * csrf_token_key: The key to use when storing the CSRF token in the session (for example,
537
+ # :dropbox_auth_csrf_token)
538
+ # * locale: The locale of the user currently using your app (ex: "en" or "en_US").
539
+ def initialize(consumer_key, consumer_secret, redirect_uri, session, csrf_token_session_key, locale=nil)
540
+ super(consumer_key, consumer_secret, locale)
541
+ if not redirect_uri.is_a?(String)
542
+ raise ArgumentError, "redirect_uri must be a String, got #{consumer_secret.inspect}"
543
+ end
544
+ @redirect_uri = redirect_uri
545
+ @session = session
546
+ @csrf_token_session_key = csrf_token_session_key
547
+ end
548
+
549
+ # Starts the OAuth 2 authorizaton process, which involves redirecting the user to
550
+ # the returned "authorization URL" (a URL on the Dropbox website). When the user then
551
+ # either approves or denies your app access, Dropbox will redirect them to the
552
+ # redirect_uri you provided to the constructor, at which point you should call finish()
553
+ # to complete the process.
554
+ #
555
+ # This function will also save a CSRF token to the session and csrf_token_session_key
556
+ # you provided to the constructor. This CSRF token will be checked on finish() to prevent
557
+ # request forgery.
558
+ #
559
+ # * url_state: Any data you would like to keep in the URL through the authorization
560
+ # process. This exact value will be returned to you by finish().
561
+ #
562
+ # Returns the URL to redirect the user to.
563
+ def start(url_state=nil)
564
+ unless url_state.nil? or url_state.is_a?(String)
565
+ raise ArgumentError, "url_state must be a String"
566
+ end
567
+
568
+ csrf_token = SecureRandom.base64(16)
569
+ state = csrf_token
570
+ unless url_state.nil?
571
+ state += "|" + url_state
572
+ end
573
+ @session[@csrf_token_session_key] = csrf_token
574
+
575
+ return _get_authorize_url(@redirect_uri, state)
576
+ end
577
+
578
+ # Call this after the user has visited the authorize URL (see: start()), approved your app,
579
+ # and was redirected to your redirect URI.
580
+ #
581
+ # * query_params: The query params on the GET request to your redirect URI.
582
+ #
583
+ # Returns a tuple of (access_token, user_id, url_state). access_token can be used to
584
+ # construct a DropboxClient. user_id is the Dropbox user ID of the user that jsut approved
585
+ # your app. url_state is the value you originally passed in to start().
586
+ #
587
+ # Can throw BadRequestError, BadStateError, CsrfError, NotApprovedError,
588
+ # ProviderError, and the standard DropboxError.
589
+ def finish(query_params)
590
+ csrf_token_from_session = @session[@csrf_token_session_key]
591
+
592
+ # Check well-formedness of request.
593
+
594
+ state = query_params['state']
595
+ if state.nil?
596
+ raise BadRequestError.new("Missing query parameter 'state'.")
597
+ end
598
+
599
+ error = query_params['error']
600
+ error_description = query_params['error_description']
601
+ code = query_params['code']
602
+
603
+ if not error.nil? and not code.nil?
604
+ raise BadRequestError.new("Query parameters 'code' and 'error' are both set;" +
605
+ " only one must be set.")
606
+ end
607
+ if error.nil? and code.nil?
608
+ raise BadRequestError.new("Neither query parameter 'code' or 'error' is set.")
609
+ end
610
+
611
+ # Check CSRF token
612
+
613
+ if csrf_token_from_session.nil?
614
+ raise BadStateError.new("Missing CSRF token in session.");
615
+ end
616
+ unless csrf_token_from_session.length > 20
617
+ raise RuntimeError.new("CSRF token unexpectedly short: #{csrf_token_from_session.inspect}")
618
+ end
619
+
620
+ split_pos = state.index('|')
621
+ if split_pos.nil?
622
+ given_csrf_token = state
623
+ url_state = nil
624
+ else
625
+ given_csrf_token, url_state = state.split('|', 2)
626
+ end
627
+ if not Dropbox::safe_string_equals(csrf_token_from_session, given_csrf_token)
628
+ raise CsrfError.new("Expected #{csrf_token_from_session.inspect}, " +
629
+ "got #{given_csrf_token.inspect}.")
630
+ end
631
+ @session.delete(@csrf_token_session_key)
632
+
633
+ # Check for error identifier
634
+
635
+ if not error.nil?
636
+ if error == 'access_denied'
637
+ # The user clicked "Deny"
638
+ if error_description.nil?
639
+ raise NotApprovedError.new("No additional description from Dropbox.")
640
+ else
641
+ raise NotApprovedError.new("Additional description from Dropbox: #{error_description}")
642
+ end
643
+ else
644
+ # All other errors.
645
+ full_message = error
646
+ if not error_description.nil?
647
+ full_message += ": " + error_description
648
+ end
649
+ raise ProviderError.new(full_message)
650
+ end
651
+ end
652
+
653
+ # If everything went ok, make the network call to get an access token.
654
+
655
+ access_token, user_id = _finish(code, @redirect_uri)
656
+ return access_token, user_id, url_state
657
+ end
658
+
659
+ # Thrown if the redirect URL was missing parameters or if the given parameters were not valid.
660
+ #
661
+ # The recommended action is to show an HTTP 400 error page.
662
+ class BadRequestError < Exception; end
663
+
664
+ # Thrown if all the parameters are correct, but there's no CSRF token in the session. This
665
+ # probably means that the session expired.
666
+ #
667
+ # The recommended action is to redirect the user's browser to try the approval process again.
668
+ class BadStateError < Exception; end
669
+
670
+ # Thrown if the given 'state' parameter doesn't contain the CSRF token from the user's session.
671
+ # This is blocked to prevent CSRF attacks.
672
+ #
673
+ # The recommended action is to respond with an HTTP 403 error page.
674
+ class CsrfError < Exception; end
675
+
676
+ # The user chose not to approve your app.
677
+ class NotApprovedError < Exception; end
678
+
679
+ # Dropbox redirected to your redirect URI with some unexpected error identifier and error
680
+ # message.
681
+ class ProviderError < Exception; end
682
+ end
683
+
684
+
685
+ # A class that represents either an OAuth request token or an OAuth access token.
686
+ class OAuthToken # :nodoc:
687
+ attr_reader :key, :secret
688
+ def initialize(key, secret)
689
+ @key = key
690
+ @secret = secret
691
+ end
692
+ end
693
+
694
+
695
+ # This is the usual error raised on any Dropbox related Errors
696
+ class DropboxError < RuntimeError
697
+ attr_accessor :http_response, :error, :user_error
698
+ def initialize(error, http_response=nil, user_error=nil)
699
+ @error = error
700
+ @http_response = http_response
701
+ @user_error = user_error
702
+ end
703
+
704
+ def to_s
705
+ return "#{user_error} (#{error})" if user_error
706
+ "#{error}"
707
+ end
708
+ end
709
+
710
+ # This is the error raised on Authentication failures. Usually this means
711
+ # one of three things
712
+ # * Your user failed to go to the authorize url and approve your application
713
+ # * You set an invalid or expired token and secret on your Session
714
+ # * Your user deauthorized the application after you stored a valid token and secret
715
+ class DropboxAuthError < DropboxError
716
+ end
717
+
718
+ # This is raised when you call metadata with a hash and that hash matches
719
+ # See documentation in metadata function
720
+ class DropboxNotModified < DropboxError
721
+ end
722
+
723
+ # Use this class to make Dropbox API calls. You'll need to obtain an OAuth 2 access token
724
+ # first; you can get one using either DropboxOAuth2Flow or DropboxOAuth2FlowNoRedirect.
725
+ class DropboxClient
726
+
727
+ # Args:
728
+ # * +oauth2_access_token+: Obtained via DropboxOAuth2Flow or DropboxOAuth2FlowNoRedirect.
729
+ # * +locale+: The user's current locale (used to localize error messages).
730
+ def initialize(oauth2_access_token, root="auto", locale=nil)
731
+ if oauth2_access_token.is_a?(String)
732
+ @session = DropboxOAuth2Session.new(oauth2_access_token, locale)
733
+ elsif oauth2_access_token.is_a?(DropboxSession)
734
+ @session = oauth2_access_token
735
+ @session.get_access_token
736
+ if not locale.nil?
737
+ @session.locale = locale
738
+ end
739
+ else
740
+ raise ArgumentError.new("oauth2_access_token doesn't have a valid type")
741
+ end
742
+
743
+ @root = root.to_s # If they passed in a symbol, make it a string
744
+
745
+ if not ["dropbox","app_folder","auto"].include?(@root)
746
+ raise ArgumentError.new("root must be :dropbox, :app_folder, or :auto")
747
+ end
748
+ if @root == "app_folder"
749
+ #App Folder is the name of the access type, but for historical reasons
750
+ #sandbox is the URL root component that indicates this
751
+ @root = "sandbox"
752
+ end
753
+ end
754
+
755
+ # Returns some information about the current user's Dropbox account (the "current user"
756
+ # is the user associated with the access token you're using).
757
+ #
758
+ # For a detailed description of what this call returns, visit:
759
+ # https://www.dropbox.com/developers/reference/api#account-info
760
+ def account_info()
761
+ response = @session.do_get "/account/info"
762
+ Dropbox::parse_response(response)
763
+ end
764
+
765
+ # Disables the access token that this +DropboxClient+ is using. If this call
766
+ # succeeds, further API calls using this object will fail.
767
+ def disable_access_token
768
+ @session.do_post "/disable_access_token"
769
+ nil
770
+ end
771
+
772
+ # If this +DropboxClient+ was created with an OAuth 1 access token, this method
773
+ # can be used to create an equivalent OAuth 2 access token. This can be used to
774
+ # upgrade your app's existing access tokens from OAuth 1 to OAuth 2.
775
+ def create_oauth2_access_token
776
+ if not @session.is_a?(DropboxSession)
777
+ raise ArgumentError.new("This call requires a DropboxClient that is configured with " \
778
+ "an OAuth 1 access token.")
779
+ end
780
+ response = @session.do_post "/oauth2/token_from_oauth1"
781
+ Dropbox::parse_response(response)['access_token']
782
+ end
783
+
784
+ # Uploads a file to a server. This uses the HTTP PUT upload method for simplicity
785
+ #
786
+ # Args:
787
+ # * +to_path+: The directory path to upload the file to. If the destination
788
+ # directory does not yet exist, it will be created.
789
+ # * +file_obj+: A file-like object to upload. If you would like, you can
790
+ # pass a string as file_obj.
791
+ # * +overwrite+: Whether to overwrite an existing file at the given path. [default is False]
792
+ # If overwrite is False and a file already exists there, Dropbox
793
+ # will rename the upload to make sure it doesn't overwrite anything.
794
+ # You must check the returned metadata to know what this new name is.
795
+ # This field should only be True if your intent is to potentially
796
+ # clobber changes to a file that you don't know about.
797
+ # * +parent_rev+: The rev field from the 'parent' of this upload. [optional]
798
+ # If your intent is to update the file at the given path, you should
799
+ # pass the parent_rev parameter set to the rev value from the most recent
800
+ # metadata you have of the existing file at that path. If the server
801
+ # has a more recent version of the file at the specified path, it will
802
+ # automatically rename your uploaded file, spinning off a conflict.
803
+ # Using this parameter effectively causes the overwrite parameter to be ignored.
804
+ # The file will always be overwritten if you send the most-recent parent_rev,
805
+ # and it will never be overwritten you send a less-recent one.
806
+ # Returns:
807
+ # * a Hash containing the metadata of the newly uploaded file. The file may have a different
808
+ # name if it conflicted.
809
+ #
810
+ # Simple Example
811
+ # client = DropboxClient(oauth2_access_token)
812
+ # #session is a DropboxSession I've already authorized
813
+ # client.put_file('/test_file_on_dropbox', open('/tmp/test_file'))
814
+ # This will upload the "/tmp/test_file" from my computer into the root of my App's app folder
815
+ # and call it "test_file_on_dropbox".
816
+ # The file will not overwrite any pre-existing file.
817
+ def upload(to_path, file_obj, mode='add')
818
+ path = "/files/upload"
819
+ params = {
820
+ 'path' => to_path,
821
+ 'mode' => mode,
822
+ 'autorename' => false,
823
+ 'mute' => false
824
+ }
825
+
826
+ headers = {"Content-Type" => "application/octet-stream",
827
+ "Dropbox-API-Arg" => params.to_json}
828
+ response = @session.do_post path, file_obj, headers, :content
829
+
830
+ Dropbox::parse_response(response)
831
+ end
832
+
833
+ # Returns a ChunkedUploader object.
834
+ #
835
+ # Args:
836
+ # * +file_obj+: The file-like object to be uploaded. Must support .read()
837
+ # * +total_size+: The total size of file_obj
838
+ def get_chunked_uploader(file_obj, total_size)
839
+ ChunkedUploader.new(self, file_obj, total_size)
840
+ end
841
+
842
+ # ChunkedUploader is responsible for uploading a large file to Dropbox in smaller chunks.
843
+ # This allows large files to be uploaded and makes allows recovery during failure.
844
+ class ChunkedUploader
845
+ attr_accessor :file_obj, :total_size, :offset, :upload_id, :client
846
+
847
+ def initialize(client, file_obj, total_size)
848
+ @client = client
849
+ @file_obj = file_obj
850
+ @total_size = total_size
851
+ @upload_id = nil
852
+ @offset = 0
853
+ end
854
+
855
+ # Uploads data from this ChunkedUploader's file_obj in chunks, until
856
+ # an error occurs. Throws an exception when an error occurs, and can
857
+ # be called again to resume the upload.
858
+ #
859
+ # Args:
860
+ # * +chunk_size+: The chunk size for each individual upload. Defaults to 4MB.
861
+ def upload(chunk_size=4*1024*1024)
862
+ last_chunk = nil
863
+
864
+ while @offset < @total_size
865
+ if not last_chunk
866
+ last_chunk = @file_obj.read(chunk_size)
867
+ end
868
+
869
+ resp = {}
870
+ begin
871
+ resp = Dropbox::parse_response(@client.partial_chunked_upload(last_chunk, @upload_id, @offset))
872
+ last_chunk = nil
873
+ rescue SocketError => e
874
+ raise e
875
+ rescue SystemCallError => e
876
+ raise e
877
+ rescue DropboxError => e
878
+ raise e if e.http_response.nil? or e.http_response.code[0] == '5'
879
+ begin
880
+ resp = JSON.parse(e.http_response.body)
881
+ raise DropboxError.new('server response does not have offset key') unless resp.has_key? 'offset'
882
+ rescue JSON::ParserError
883
+ raise DropboxError.new("Unable to parse JSON response: #{e.http_response.body}")
884
+ end
885
+ end
886
+
887
+ if resp.has_key? 'offset' and resp['offset'] > @offset
888
+ @offset += (resp['offset'] - @offset) if resp['offset']
889
+ last_chunk = nil
890
+ end
891
+ @upload_id = resp['upload_id'] if resp['upload_id']
892
+ end
893
+ end
894
+
895
+ # Completes a file upload
896
+ #
897
+ # Args:
898
+ # * +to_path+: The directory path to upload the file to. If the destination
899
+ # directory does not yet exist, it will be created.
900
+ # * +overwrite+: Whether to overwrite an existing file at the given path. [default is False]
901
+ # If overwrite is False and a file already exists there, Dropbox
902
+ # will rename the upload to make sure it doesn't overwrite anything.
903
+ # You must check the returned metadata to know what this new name is.
904
+ # This field should only be True if your intent is to potentially
905
+ # clobber changes to a file that you don't know about.
906
+ # * parent_rev: The rev field from the 'parent' of this upload.
907
+ # If your intent is to update the file at the given path, you should
908
+ # pass the parent_rev parameter set to the rev value from the most recent
909
+ # metadata you have of the existing file at that path. If the server
910
+ # has a more recent version of the file at the specified path, it will
911
+ # automatically rename your uploaded file, spinning off a conflict.
912
+ # Using this parameter effectively causes the overwrite parameter to be ignored.
913
+ # The file will always be overwritten if you send the most-recent parent_rev,
914
+ # and it will never be overwritten you send a less-recent one.
915
+ #
916
+ # Returns:
917
+ # * A Hash with the metadata of file just uploaded.
918
+ # For a detailed description of what this call returns, visit:
919
+ # https://www.dropbox.com/developers/reference/api#metadata
920
+ def finish(to_path, overwrite=false, parent_rev=nil)
921
+ response = @client.commit_chunked_upload(to_path, @upload_id, overwrite, parent_rev)
922
+ Dropbox::parse_response(response)
923
+ end
924
+ end
925
+
926
+ def commit_chunked_upload(to_path, upload_id, overwrite=false, parent_rev=nil) #:nodoc
927
+ path = "/commit_chunked_upload/#{@root}#{format_path(to_path)}"
928
+ params = {'overwrite' => overwrite.to_s,
929
+ 'upload_id' => upload_id,
930
+ 'parent_rev' => parent_rev
931
+ }
932
+ headers = nil
933
+ @session.do_post path, params, headers, :content
934
+ end
935
+
936
+ def partial_chunked_upload(data, upload_id=nil, offset=nil) #:nodoc
937
+ params = {
938
+ 'upload_id' => upload_id,
939
+ 'offset' => offset,
940
+ }
941
+ headers = {'Content-Type' => "application/octet-stream"}
942
+ @session.do_put '/chunked_upload', params, headers, data, :content
943
+ end
944
+
945
+ # Download a file
946
+ #
947
+ # Args:
948
+ # * +from_path+: The path to the file to be downloaded
949
+ # * +rev+: A previous revision value of the file to be downloaded
950
+ #
951
+ # Returns:
952
+ # * The file contents.
953
+ # GEM 8/18/2016 - DEPRECIATED
954
+ def get_file(from_path, rev=nil)
955
+ response = get_file_impl(from_path, rev)
956
+ Dropbox::parse_response(response, raw=true)
957
+ end
958
+
959
+ # Download a file
960
+ #
961
+ # Args:
962
+ # * +from_path+: The path to the file to be downloaded
963
+ #
964
+ # Returns:
965
+ # * The HTTPResponse for the file download request.
966
+ def download from_path # :nodoc:
967
+ path = "/files/download"
968
+ headers = {"Dropbox-API-Arg" => {'path' => from_path}.to_json}
969
+ response = @session.do_post path, nil, headers, :content
970
+ parsed_response = Dropbox::parse_response(response, raw=true)
971
+ metadata = parse_metadata(response)
972
+ return parsed_response, metadata
973
+ end
974
+
975
+ # Parses out file metadata from a raw dropbox HTTP response.
976
+ #
977
+ # Args:
978
+ # * +dropbox_raw_response+: The raw, unparsed HTTPResponse from Dropbox.
979
+ #
980
+ # Returns:
981
+ # * The metadata of the file as a hash.
982
+ def parse_metadata(dropbox_raw_response) # :nodoc:
983
+ begin
984
+ raw_metadata = dropbox_raw_response['dropbox-api-result']
985
+ metadata = JSON.parse(raw_metadata)
986
+ rescue StandardError => msg
987
+ raise DropboxError.new("Dropbox Server Error: dropbox-api-result=#{raw_metadata}",
988
+ dropbox_raw_response)
989
+ end
990
+ metadata
991
+ end
992
+ private :parse_metadata
993
+
994
+ # Copy a file or folder to a new location.
995
+ #
996
+ # Args:
997
+ # * +from_path+: The path to the file or folder to be copied.
998
+ # * +to_path+: The destination path of the file or folder to be copied.
999
+ # This parameter should include the destination filename (e.g.
1000
+ # from_path: '/test.txt', to_path: '/dir/test.txt'). If there's
1001
+ # already a file at the to_path, this copy will be renamed to
1002
+ # be unique.
1003
+ #
1004
+ # Returns:
1005
+ # * A hash with the metadata of the new copy of the file or folder.
1006
+ # For a detailed description of what this call returns, visit:
1007
+ # https://www.dropbox.com/developers/reference/api#fileops-copy
1008
+ def file_copy(from_path, to_path)
1009
+ params = {
1010
+ "root" => @root,
1011
+ "from_path" => format_path(from_path, false),
1012
+ "to_path" => format_path(to_path, false),
1013
+ }
1014
+ response = @session.do_post "/fileops/copy", params
1015
+ Dropbox::parse_response(response)
1016
+ end
1017
+
1018
+ # Create a folder.
1019
+ #
1020
+ # Arguments:
1021
+ # * +path+: The path of the new folder.
1022
+ #
1023
+ # Returns:
1024
+ # * A hash with the metadata of the newly created folder.
1025
+ # For a detailed description of what this call returns, visit:
1026
+ # https://www.dropbox.com/developers/reference/api#fileops-create-folder
1027
+ def file_create_folder(path)
1028
+ params = {
1029
+ "root" => @root,
1030
+ "path" => format_path(path, false),
1031
+ }
1032
+ response = @session.do_post "/fileops/create_folder", params
1033
+
1034
+ Dropbox::parse_response(response)
1035
+ end
1036
+
1037
+ # Deletes a file
1038
+ #
1039
+ # Arguments:
1040
+ # * +path+: The path of the file to delete
1041
+ #
1042
+ # Returns:
1043
+ # * A Hash with the metadata of file just deleted.
1044
+ # For a detailed description of what this call returns, visit:
1045
+ # https://www.dropbox.com/developers/reference/api#fileops-delete
1046
+ def file_delete(path)
1047
+ params = {
1048
+ "root" => @root,
1049
+ "path" => format_path(path, false),
1050
+ }
1051
+ response = @session.do_post "/fileops/delete", params
1052
+ Dropbox::parse_response(response)
1053
+ end
1054
+
1055
+ # Moves a file
1056
+ #
1057
+ # Arguments:
1058
+ # * +from_path+: The path of the file to be moved
1059
+ # * +to_path+: The destination path of the file or folder to be moved
1060
+ # If the file or folder already exists, it will be renamed to be unique.
1061
+ #
1062
+ # Returns:
1063
+ # * A Hash with the metadata of file or folder just moved.
1064
+ # For a detailed description of what this call returns, visit:
1065
+ # https://www.dropbox.com/developers/reference/api#fileops-delete
1066
+ def file_move(from_path, to_path)
1067
+ params = {
1068
+ "root" => @root,
1069
+ "from_path" => format_path(from_path, false),
1070
+ "to_path" => format_path(to_path, false),
1071
+ }
1072
+ response = @session.do_post "/fileops/move", params
1073
+ Dropbox::parse_response(response)
1074
+ end
1075
+
1076
+ # Retrives metadata for a file or folder
1077
+ #
1078
+ # Arguments:
1079
+ # * path: The path to the file or folder.
1080
+ # * list: Whether to list all contained files (only applies when
1081
+ # path refers to a folder).
1082
+ # * file_limit: The maximum number of file entries to return within
1083
+ # a folder. If the number of files in the directory exceeds this
1084
+ # limit, an exception is raised. The server will return at max
1085
+ # 25,000 files within a folder.
1086
+ # * hash: Every directory listing has a hash parameter attached that
1087
+ # can then be passed back into this function later to save on
1088
+ # bandwidth. Rather than returning an unchanged folder's contents, if
1089
+ # the hash matches a DropboxNotModified exception is raised.
1090
+ # * rev: Optional. The revision of the file to retrieve the metadata for.
1091
+ # This parameter only applies for files. If omitted, you'll receive
1092
+ # the most recent revision metadata.
1093
+ # * include_deleted: Specifies whether to include deleted files in metadata results.
1094
+ # * include_media_info: Specifies to include media info, such as time_taken for photos
1095
+ #
1096
+ # Returns:
1097
+ # * A Hash object with the metadata of the file or folder (and contained files if
1098
+ # appropriate). For a detailed description of what this call returns, visit:
1099
+ # https://www.dropbox.com/developers/reference/api#metadata
1100
+ def metadata(path, file_limit=25000, list=true, hash=nil, rev=nil, include_deleted=false, include_media_info=false)
1101
+ params = {
1102
+ "file_limit" => file_limit.to_s,
1103
+ "list" => list.to_s,
1104
+ "include_deleted" => include_deleted.to_s,
1105
+ "hash" => hash,
1106
+ "rev" => rev,
1107
+ "include_media_info" => include_media_info
1108
+ }
1109
+
1110
+ response = @session.do_get "/metadata/#{@root}#{format_path(path)}", params
1111
+ if response.kind_of? Net::HTTPRedirection
1112
+ raise DropboxNotModified.new("metadata not modified")
1113
+ end
1114
+ Dropbox::parse_response(response)
1115
+ end
1116
+
1117
+ # Search directory for filenames matching query
1118
+ #
1119
+ # Arguments:
1120
+ # * path: The directory to search within
1121
+ # * query: The query to search on (3 character minimum)
1122
+ # * file_limit: The maximum number of file entries to return/
1123
+ # If the number of files exceeds this
1124
+ # limit, an exception is raised. The server will return at max 1,000
1125
+ # * include_deleted: Whether to include deleted files in search results
1126
+ #
1127
+ # Returns:
1128
+ # * A Hash object with a list the metadata of the file or folders matching query
1129
+ # inside path. For a detailed description of what this call returns, visit:
1130
+ # https://www.dropbox.com/developers/reference/api#search
1131
+ def search(path, query, file_limit=1000)
1132
+ params = {
1133
+ 'path' => path,
1134
+ 'query' => query,
1135
+ 'start' => 0,
1136
+ 'max_results' => file_limit,
1137
+ 'mode' => 'filename'
1138
+ }.to_json
1139
+ headers = {"Content-Type" => "application/json"}
1140
+
1141
+ response = @session.do_post "/files/search", params, headers
1142
+ Dropbox::parse_response(response)
1143
+ end
1144
+
1145
+ # Retrive revisions of a file
1146
+ #
1147
+ # Arguments:
1148
+ # * path: The file to fetch revisions for. Note that revisions
1149
+ # are not available for folders.
1150
+ # * rev_limit: The maximum number of file entries to return within
1151
+ # a folder. The server will return at max 1,000 revisions.
1152
+ #
1153
+ # Returns:
1154
+ # * A Hash object with a list of the metadata of the all the revisions of
1155
+ # all matches files (up to rev_limit entries)
1156
+ # For a detailed description of what this call returns, visit:
1157
+ # https://www.dropbox.com/developers/reference/api#revisions
1158
+ def revisions(path, rev_limit=1000)
1159
+ params = {
1160
+ 'rev_limit' => rev_limit.to_s
1161
+ }
1162
+
1163
+ response = @session.do_get "/revisions/#{@root}#{format_path(path)}", params
1164
+ Dropbox::parse_response(response)
1165
+ end
1166
+
1167
+ # Restore a file to a previous revision.
1168
+ #
1169
+ # Arguments:
1170
+ # * path: The file to restore. Note that folders can't be restored.
1171
+ # * rev: A previous rev value of the file to be restored to.
1172
+ #
1173
+ # Returns:
1174
+ # * A Hash object with a list the metadata of the file or folders restored
1175
+ # For a detailed description of what this call returns, visit:
1176
+ # https://www.dropbox.com/developers/reference/api#search
1177
+ def restore(path, rev)
1178
+ params = {
1179
+ 'rev' => rev.to_s
1180
+ }
1181
+
1182
+ response = @session.do_post "/restore/#{@root}#{format_path(path)}", params
1183
+ Dropbox::parse_response(response)
1184
+ end
1185
+
1186
+ # Returns a direct link to a media file
1187
+ # All of Dropbox's API methods require OAuth, which may cause problems in
1188
+ # situations where an application expects to be able to hit a URL multiple times
1189
+ # (for example, a media player seeking around a video file). This method
1190
+ # creates a time-limited URL that can be accessed without any authentication.
1191
+ #
1192
+ # Arguments:
1193
+ # * path: The file to stream.
1194
+ #
1195
+ # Returns:
1196
+ # * A Hash object that looks like the following:
1197
+ # {'url': 'https://dl.dropboxusercontent.com/1/view/abcdefghijk/example', 'expires': 'Thu, 16 Sep 2011 01:01:25 +0000'}
1198
+ def media(path)
1199
+ response = @session.do_get "/media/#{@root}#{format_path(path)}"
1200
+ Dropbox::parse_response(response)
1201
+ end
1202
+
1203
+ # Get a URL to share a media file
1204
+ # Shareable links created on Dropbox are time-limited, but don't require any
1205
+ # authentication, so they can be given out freely. The time limit should allow
1206
+ # at least a day of shareability, though users have the ability to disable
1207
+ # a link from their account if they like.
1208
+ #
1209
+ # Arguments:
1210
+ # * path: The file to share.
1211
+ # * short_url: When true (default), the url returned will be shortened using the Dropbox url shortener. If false,
1212
+ # the url will link directly to the file's preview page.
1213
+ #
1214
+ # Returns:
1215
+ # * A Hash object that looks like the following example:
1216
+ # {'url': 'https://db.tt/c0mFuu1Y', 'expires': 'Tue, 01 Jan 2030 00:00:00 +0000'}
1217
+ # For a detailed description of what this call returns, visit:
1218
+ # https://www.dropbox.com/developers/reference/api#shares
1219
+ def shares(path, short_url=true)
1220
+ response = @session.do_get "/shares/#{@root}#{format_path(path)}", {"short_url"=>short_url}
1221
+ Dropbox::parse_response(response)
1222
+ end
1223
+
1224
+ # Download a PDF or HTML preview for a file.
1225
+ #
1226
+ # Arguments:
1227
+ # * path: The path to the file to be previewed.
1228
+ # * rev: Optional. The revision of the file to retrieve the metadata for.
1229
+ # If omitted, you'll get the most recent version.
1230
+ # Returns:
1231
+ # * The preview data
1232
+ def preview(path, rev=nil)
1233
+ path = "/previews/#{@root}#{format_path(path)}"
1234
+ params = { 'rev' => rev }
1235
+ response = @session.do_get path, params, :content
1236
+ Dropbox::parse_response(response, raw=true)
1237
+ end
1238
+
1239
+ # Download a thumbnail for an image.
1240
+ #
1241
+ # Arguments:
1242
+ # * from_path: The path to the file to be thumbnailed.
1243
+ # * size: A string describing the desired thumbnail size. At this time,
1244
+ # 'small' (32x32), 'medium' (64x64), 'large' (128x128), 's' (64x64),
1245
+ # 'm' (128x128), 'l' (640x640), and 'xl' (1024x1024) are officially supported sizes.
1246
+ # Check https://www.dropbox.com/developers/reference/api#thumbnails
1247
+ # for more details. [defaults to large]
1248
+ # Returns:
1249
+ # * The thumbnail data
1250
+ def thumbnail(from_path, size='large')
1251
+ response = thumbnail_impl(from_path, size)
1252
+ Dropbox::parse_response(response, raw=true)
1253
+ end
1254
+
1255
+ # Download a thumbnail for an image along with the image's metadata.
1256
+ #
1257
+ # Arguments:
1258
+ # * from_path: The path to the file to be thumbnailed.
1259
+ # * size: A string describing the desired thumbnail size. See thumbnail()
1260
+ # for details.
1261
+ # Returns:
1262
+ # * The thumbnail data
1263
+ # * The metadata for the image as a hash
1264
+ def thumbnail_and_metadata(from_path, size='large')
1265
+ response = thumbnail_impl(from_path, size)
1266
+ parsed_response = Dropbox::parse_response(response, raw=true)
1267
+ metadata = parse_metadata(response)
1268
+ return parsed_response, metadata
1269
+ end
1270
+
1271
+ # A way of letting you keep a local representation of the Dropbox folder
1272
+ # heirarchy. You can periodically call delta() to get a list of "delta
1273
+ # entries", which are instructions on how to update your local state to
1274
+ # match the server's state.
1275
+ #
1276
+ # Arguments:
1277
+ # * +cursor+: On the first call, omit this argument (or pass in +nil+). On
1278
+ # subsequent calls, pass in the +cursor+ string returned by the previous
1279
+ # call.
1280
+ # * +path_prefix+: If provided, results will be limited to files and folders
1281
+ # whose paths are equal to or under +path_prefix+. The +path_prefix+ is
1282
+ # fixed for a given cursor. Whatever +path_prefix+ you use on the first
1283
+ # +delta()+ must also be passed in on subsequent calls that use the returned
1284
+ # cursor.
1285
+ #
1286
+ # Returns: A hash with three fields.
1287
+ # * +entries+: A list of "delta entries" (described below)
1288
+ # * +reset+: If +true+, you should reset local state to be an empty folder
1289
+ # before processing the list of delta entries. This is only +true+ only
1290
+ # in rare situations.
1291
+ # * +cursor+: A string that is used to keep track of your current state.
1292
+ # On the next call to delta(), pass in this value to return entries
1293
+ # that were recorded since the cursor was returned.
1294
+ # * +has_more+: If +true+, then there are more entries available; you can
1295
+ # call delta() again immediately to retrieve those entries. If +false+,
1296
+ # then wait at least 5 minutes (preferably longer) before checking again.
1297
+ #
1298
+ # Delta Entries: Each entry is a 2-item list of one of following forms:
1299
+ # * [_path_, _metadata_]: Indicates that there is a file/folder at the given
1300
+ # path. You should add the entry to your local state. (The _metadata_
1301
+ # value is the same as what would be returned by the #metadata() call.)
1302
+ # * If the path refers to parent folders that don't yet exist in your
1303
+ # local state, create those parent folders in your local state. You
1304
+ # will eventually get entries for those parent folders.
1305
+ # * If the new entry is a file, replace whatever your local state has at
1306
+ # _path_ with the new entry.
1307
+ # * If the new entry is a folder, check what your local state has at
1308
+ # _path_. If it's a file, replace it with the new entry. If it's a
1309
+ # folder, apply the new _metadata_ to the folder, but do not modify
1310
+ # the folder's children.
1311
+ # * [path, +nil+]: Indicates that there is no file/folder at the _path_ on
1312
+ # Dropbox. To update your local state to match, delete whatever is at
1313
+ # _path_, including any children (you will sometimes also get separate
1314
+ # delta entries for each child, but this is not guaranteed). If your
1315
+ # local state doesn't have anything at _path_, ignore this entry.
1316
+ #
1317
+ # Remember: Dropbox treats file names in a case-insensitive but case-preserving
1318
+ # way. To facilitate this, the _path_ strings above are lower-cased versions of
1319
+ # the actual path. The _metadata_ dicts have the original, case-preserved path.
1320
+ def delta(cursor=nil, path_prefix=nil)
1321
+ params = {
1322
+ 'cursor' => cursor,
1323
+ 'path_prefix' => path_prefix,
1324
+ }
1325
+
1326
+ response = @session.do_post "/delta", params
1327
+ Dropbox::parse_response(response)
1328
+ end
1329
+
1330
+ # A way to quickly get a cursor for the server's state, for use with #delta.
1331
+ # Unlike #delta, #delta_latest_cursor does not return any entries, so your app
1332
+ # will not know about any existing files or folders in the Dropbox account. For
1333
+ # example, if your app processes future delta entries and sees that a folder was
1334
+ # deleted, your app won't know what files were in that folder. Use this endpoint
1335
+ # if your app only needs to know about new files and modifications and doesn't
1336
+ # need to know about files that already exist in Dropbox.
1337
+
1338
+ # Arguments:
1339
+ # * +path_prefix+: If present, the returned cursor will be encoded with a
1340
+ # path_prefix for the specified path for use with #delta.
1341
+ #
1342
+ # Returns: A hash with one field.
1343
+ # * +cursor+: A string that encodes the latest server state, as would be
1344
+ # returned by #delta when "has_more" is false.
1345
+ def delta_latest_cursor(path_prefix=nil)
1346
+ params = {
1347
+ 'path_prefix' => path_prefix
1348
+ }
1349
+
1350
+ response = @session.do_post "/delta/latest_cursor", params
1351
+ Dropbox::parse_response(response)
1352
+ end
1353
+
1354
+ # Calls the long-poll endpoint which waits for changes on an account. In
1355
+ # conjunction with #delta, this call gives you a low-latency way to monitor
1356
+ # an account for file changes.
1357
+ #
1358
+ # The passed in cursor can only be acquired via a call to #delta
1359
+ #
1360
+ # Arguments:
1361
+ # * +cursor+: A delta cursor as returned from a call to #delta
1362
+ # * +timeout+: An optional integer indicating a timeout, in seconds. The
1363
+ # default value is 30 seconds, which is also the minimum allowed value. The
1364
+ # maximum is 480 seconds.
1365
+ #
1366
+ # Returns: A hash with one or two fields.
1367
+ # * +changes+: A boolean value indicating whether new changes are available.
1368
+ # * +backoff+: If present, indicates how many seconds your code should wait
1369
+ # before calling #longpoll_delta again.
1370
+ def longpoll_delta(cursor, timeout=30)
1371
+ params = {
1372
+ 'cursor' => cursor,
1373
+ 'timeout' => timeout
1374
+ }
1375
+
1376
+ response = @session.do_get "/longpoll_delta", params, :notify
1377
+ Dropbox::parse_response(response)
1378
+ end
1379
+
1380
+ # Download a thumbnail (helper method - don't call this directly).
1381
+ #
1382
+ # Args:
1383
+ # * +from_path+: The path to the file to be thumbnailed.
1384
+ # * +size+: A string describing the desired thumbnail size. See thumbnail()
1385
+ # for details.
1386
+ #
1387
+ # Returns:
1388
+ # * The HTTPResponse for the thumbnail request.
1389
+ def thumbnail_impl(from_path, size='large') # :nodoc:
1390
+ path = "/thumbnails/#{@root}#{format_path(from_path, true)}"
1391
+ params = {
1392
+ "size" => size
1393
+ }
1394
+ @session.do_get path, params, :content
1395
+ end
1396
+ private :thumbnail_impl
1397
+
1398
+
1399
+ # Creates and returns a copy ref for a specific file. The copy ref can be
1400
+ # used to instantly copy that file to the Dropbox of another account.
1401
+ #
1402
+ # Args:
1403
+ # * +path+: The path to the file for a copy ref to be created on.
1404
+ #
1405
+ # Returns:
1406
+ # * A Hash object that looks like the following example:
1407
+ # {"expires"=>"Fri, 31 Jan 2042 21:01:05 +0000", "copy_ref"=>"z1X6ATl6aWtzOGq0c3g5Ng"}
1408
+ def create_copy_ref(path)
1409
+ path = "/copy_ref/#{@root}#{format_path(path)}"
1410
+ response = @session.do_get path
1411
+ Dropbox::parse_response(response)
1412
+ end
1413
+
1414
+ # Adds the file referenced by the copy ref to the specified path
1415
+ #
1416
+ # Args:
1417
+ # * +copy_ref+: A copy ref string that was returned from a create_copy_ref call.
1418
+ # The copy_ref can be created from any other Dropbox account, or from the same account.
1419
+ # * +to_path+: The path to where the file will be created.
1420
+ #
1421
+ # Returns:
1422
+ # * A hash with the metadata of the new file.
1423
+ def add_copy_ref(to_path, copy_ref)
1424
+ params = {'from_copy_ref' => copy_ref,
1425
+ 'to_path' => "#{to_path}",
1426
+ 'root' => @root}
1427
+
1428
+ response = @session.do_post "/fileops/copy", params
1429
+ Dropbox::parse_response(response)
1430
+ end
1431
+
1432
+ # Save a file from the specified URL into Dropbox. If the given path already
1433
+ # exists, the file will be renamed to avoid the conflict (e.g. myfile (1).txt).
1434
+ #
1435
+ # Args:
1436
+ # * +to_path+: The path in Dropbox where the file will be saved (e.g. /folder/file.ext).
1437
+ # * +url+: The URL to be fetched.
1438
+ #
1439
+ # Returns:
1440
+ # * A dictionary with a status and job. The status is as defined in the
1441
+ # /save_url_job documentation. The job field gives a job ID that can be used
1442
+ # with the /save_url_job endpoint to check the job's status.
1443
+ # Check https://www.dropbox.com/developers/core/docs#save-url for more info.
1444
+ # {"status": "PENDING", "job": "PEiuxsfaISEAAAAAAADwzg"}
1445
+ def save_url(to_path, url)
1446
+ params = { 'url' => url }
1447
+
1448
+ response = @session.do_post "/save_url/auto#{format_path(to_path, true)}", params
1449
+ Dropbox::parse_response(response)
1450
+ end
1451
+
1452
+ # Check the status of a save URL job.
1453
+ #
1454
+ # Args:
1455
+ # * +job_id+: A job ID returned from /save_url.
1456
+ #
1457
+ # Returns:
1458
+ # *A dictionary with a status field with one of the following values:
1459
+ # PENDING, DOWNLOADING, COMPLETE, FAILED
1460
+ # Check https://www.dropbox.com/developers/core/docs#save-url for more info.
1461
+ # {"status": "FAILED", "error": "Job timed out"}
1462
+ def save_url_job(job_id)
1463
+ response = @session.do_get "/save_url_job/#{job_id}"
1464
+ Dropbox::parse_response(response)
1465
+ end
1466
+
1467
+ #From the oauth spec plus "/". Slash should not be ecsaped
1468
+ RESERVED_CHARACTERS = /[^a-zA-Z0-9\-\.\_\~\/]/ # :nodoc:
1469
+
1470
+ def format_path(path, escape=true) # :nodoc:
1471
+ path = path.gsub(/\/+/,"/")
1472
+ # replace multiple slashes with a single one
1473
+
1474
+ path = path.gsub(/^\/?/,"/")
1475
+ # ensure the path starts with a slash
1476
+
1477
+ path.gsub(/\/?$/,"")
1478
+ # ensure the path doesn't end with a slash
1479
+
1480
+ return URI.escape(path, RESERVED_CHARACTERS) if escape
1481
+ path
1482
+ end
1483
+
1484
+ end