dropbox-sdk-sv 0.0.1

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