dropbox-sdk-sv 0.0.1

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,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