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