dropbox-sdk-forked_v2 1.0.0

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