This repository has been archived by the owner on May 4, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 49
/
api.rb
621 lines (538 loc) · 22.2 KB
/
api.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
# Defines the Dropbox::API module.
require 'json'
require 'net/http/post/multipart'
module Dropbox
# Extensions to the Dropbox::Session class that add core Dropbox API
# functionality to this class. You must have authenticated your
# Dropbox::Session instance before you can call any of these methods. (See the
# Dropbox::Session class documentation for instructions.)
#
# API methods generally return +Struct+ objects containing their results,
# unless otherwise noted. See the Dropbox API documentation at
# http://developers.dropbox.com for specific information on the schema of each
# result.
#
# You can opt-in to memoization of API method results. See the
# Dropbox::Memoization class documentation to learn more.
#
# == Modes
#
# The Dropbox API works in three modes: sandbox, Dropbox (root), and
# metadata-only.
#
# * In sandbox mode (the default), all operations are rooted from your
# application's sandbox folder; other files elsewhere on the user's Dropbox
# are inaccessible.
# * In Dropbox mode, the root is the user's Dropbox folder, and all files are
# accessible. This mode is typically only available to certain API users.
# * In metadata-only mode, the root is the Dropbox folder, but write access
# is not available. Operations that modify the user's files will
# fail.
#
# You should configure the Dropbox::Session instance to use whichever mode
# you chose when you set up your application:
#
# session.mode = :metadata_only
#
# Valid values are listed in Dropbox::API::MODES, and this step is not
# necessary for sandboxed applications, as the sandbox mode is the default.
#
# You can also temporarily change the mode for many method calls using their
# options hash:
#
# session.move 'my_file', 'new/path', :mode => :dropbox
module API
include Dropbox::Memoization
# Valid API modes for the #mode= method.
MODES = [ :sandbox, :dropbox, :metadata_only ]
# Returns a Dropbox::Entry instance that can be used to work with files or
# directories in an object-oriented manner.
def entry(path)
Dropbox::Entry.new(self, path)
end
alias :file :entry
alias :directory :entry
alias :dir :entry
# Returns a +Struct+ with information about the user's account. See
# https://www.dropbox.com/developers/docs#account-info for more information
# on the data returned.
def account
get('account', 'info', :ssl => @ssl).to_struct_recursively
end
memoize :account
# Downloads the file at the given path relative to the configured mode's
# root.
#
# Returns the contents of the downloaded file as a +String+. Support for
# streaming downloads and range queries is available server-side, but not
# available in this API client due to limitations of the OAuth gem.
#
# Options:
#
# +mode+:: Temporarily changes the API mode. See the MODES array.
def download(path, options={})
path = path.sub(/^\//, '')
rest = Dropbox.check_path(path).split('/')
rest << { :ssl => @ssl }
api_body :get, 'files', root(options), *rest
#TODO streaming, range queries
end
# Downloads a minimized thumbnail for a file. Pass the path to the file,
# optionally the size of the thumbnail you want, and any additional options.
# See https://www.dropbox.com/developers/docs#thumbnails for a list of valid
# size specifiers.
#
# Returns the content of the thumbnail image as a +String+. The thumbnail
# data is in JPEG format. Returns +nil+ if the file does not have a
# thumbnail. You can check if a file has a thumbnail using the metadata
# method.
#
# Because of the way this API method works, if you pass in the name of a
# file that does not exist, you will not receive a 404, but instead just get
# +nil+.
#
# Options:
#
# +format+:: The image format (see the API documentation for supported
# formats).
# +mode+:: Temporarily changes the API mode. See the MODES array.
#
# Examples:
#
# Get the thumbnail for an image (default thunmbnail size):
#
# session.thumbnail('my/image.jpg')
#
# Get the thumbnail for an image in the +medium+ size:
#
# session.thumbnail('my/image.jpg', 'medium')
def thumbnail(path, options={})\
path = path.sub(/^\//, '')
rest = Dropbox.check_path(path).split('/')
rest << { :ssl => @ssl }
rest.last.merge! options
begin
api_body :get, 'thumbnails', root(options), *rest
rescue Dropbox::UnsuccessfulResponseError => e
raise unless e.response.code.to_i == 404
return nil
end
end
# Uploads a file to a path relative to the configured mode's root. The
# +remote_path+ parameter is taken to be the path portion _only_; the name
# of the remote file will be identical to that of the local file. You can
# provide any of the following for the first parameter:
#
# * a +File+ object (in which case the name of the local file is used),
# * a path to a file (in which case that file's name is used), or
# * a +StringIO+ (in which case the <tt>:as</tt> option must be specified.
#
# Options:
#
# +mode+:: Temporarily changes the API mode. See the MODES array.
# +as+:: Specify a custom name for the uploaded file (required when
# uploading from a +StringIO+ stream).
# +timeout+:: The amount of time to wait for a response from the Dropbox
# server (in seconds). By default it's 60; set this higher when
# uploading large files.
#
# Examples:
#
# session.upload 'music.pdf', '/' # upload a file by path to the root directory
# session.upload 'music.pdf, 'music/' # upload a file by path to the music folder
# session.upload File.new('music.pdf'), '/' # same as the first example
# session.upload open('http://www.example.com/index.html'), :as => 'example.html' # upload from a StringIO stream (requires open-uri)
def upload(local_file, remote_path, options={})
if local_file.kind_of?(File) or local_file.kind_of?(Tempfile) then
file = local_file
name = local_file.respond_to?(:original_filename) ? local_file.original_filename : File.basename(local_file.path)
local_path = local_file.path
elsif local_file.kind_of?(String) then
file = File.new(local_file)
name = File.basename(local_file)
local_path = local_file
elsif local_file.kind_of?(StringIO) then
raise(ArgumentError, "Must specify the :as option when uploading from StringIO") unless options[:as]
file = local_file
local_path = options[:as]
# hack for bug in UploadIO
class << file
attr_accessor :path
end
file.path = local_path
else
raise ArgumentError, "local_file must be a File, StringIO, or file path"
end
name = File.basename(options.delete(:as)) if options[:as]
remote_path = remote_path.sub(/^\//, '')
remote_path = Dropbox.check_path(remote_path).split('/')
remote_path << { :ssl => @ssl }
url = Dropbox.api_url('files', root(options), *remote_path)
uri = URI.parse(url)
oauth_request = Net::HTTP::Post.new(uri.path)
oauth_request.set_form_data 'file' => name
alternate_host_session = clone_with_host(@ssl ? Dropbox::ALTERNATE_SSL_HOSTS['files'] : Dropbox::ALTERNATE_HOSTS['files'])
alternate_host_session.instance_variable_get(:@consumer).sign!(oauth_request, @access_token)
oauth_signature = oauth_request.to_hash['authorization']
request = Net::HTTP::Post::Multipart.new(uri.path,
'file' => UploadIO.new(
file,
'application/octet-stream',
name))
request['authorization'] = oauth_signature.join(', ')
proxy = URI.parse(@proxy || "")
http = Net::HTTP::Proxy(proxy.host, proxy.port).new(uri.host, uri.port)
http.use_ssl = @ssl
http.read_timeout = options[:timeout] if options[:timeout]
response = http.request(request)
if response.kind_of?(Net::HTTPSuccess) then
begin
return JSON.parse(response.body).symbolize_keys_recursively.to_struct_recursively
rescue JSON::ParserError
raise ParseError.new(uri.to_s, response)
end
else
raise UnsuccessfulResponseError.new(uri.to_s, response)
end
end
# Copies the +source+ file to the path at +target+. If +target+ ends with a
# slash, the new file will share the same name as the old file. Returns a
# +Struct+ with metadata for the new file. (See the metadata method.)
#
# Both paths are assumed to be relative to the configured mode's root.
#
# Raises FileNotFoundError if +source+ does not exist. Raises
# FileExistsError if +target+ already exists.
#
# Options:
#
# +mode+:: Temporarily changes the API mode. See the MODES array.
#
# TODO The API documentation says this method returns 404/403 if the source or target is invalid, but it actually returns 5xx.
def copy(source, target, options={})
source = source.sub(/^\//, '')
target = target.sub(/^\//, '')
target << File.basename(source) if target.ends_with?('/')
begin
parse_metadata(post('fileops', 'copy', :from_path => Dropbox.check_path(source), :to_path => Dropbox.check_path(target), :root => root(options), :ssl => @ssl)).to_struct_recursively
rescue UnsuccessfulResponseError => error
raise FileNotFoundError.new(source) if error.response.kind_of?(Net::HTTPNotFound)
raise FileExistsError.new(target) if error.response.kind_of?(Net::HTTPForbidden)
raise error
end
end
alias :cp :copy
# Creates a folder at the given path. The path is assumed to be relative to
# the configured mode's root. Returns a +Struct+ with metadata about the new
# folder. (See the metadata method.)
#
# Raises FileExistsError if there is already a file or folder at +path+.
#
# Options:
#
# +mode+:: Temporarily changes the API mode. See the MODES array.
#
# TODO The API documentation says this method returns 403 if the path already exists, but it actually appends " (1)" to the end of the name and returns 200.
def create_folder(path, options={})
path = path.sub(/^\//, '')
path.sub! /\/$/, ''
begin
parse_metadata(post('fileops', 'create_folder', :path => Dropbox.check_path(path), :root => root(options), :ssl => @ssl)).to_struct_recursively
rescue UnsuccessfulResponseError => error
raise FileExistsError.new(path) if error.response.kind_of?(Net::HTTPForbidden)
raise error
end
end
alias :mkdir :create_folder
# Deletes a file or folder at the given path. The path is assumed to be
# relative to the configured mode's root.
#
# Raises FileNotFoundError if the file or folder does not exist at +path+.
#
# Options:
#
# +mode+:: Temporarily changes the API mode. See the MODES array.
#
# TODO The API documentation says this method returns 404 if the path does not exist, but it actually returns 5xx.
def delete(path, options={})
path = path.sub(/^\//, '')
path.sub! /\/$/, ''
begin
api_response(:post, 'fileops', 'delete', :path => Dropbox.check_path(path), :root => root(options), :ssl => @ssl)
rescue UnsuccessfulResponseError => error
raise FileNotFoundError.new(path) if error.response.kind_of?(Net::HTTPNotFound)
raise error
end
return true
end
alias :rm :delete
# Moves the +source+ file to the path at +target+. If +target+ ends with a
# slash, the file name will remain unchanged. If +source+ and +target+ share
# the same path but have differing file names, the file will be renamed (see
# also the rename method). Returns a +Struct+ with metadata for the new
# file. (See the metadata method.)
#
# Both paths are assumed to be relative to the configured mode's root.
#
# Raises FileNotFoundError if +source+ does not exist. Raises
# FileExistsError if +target+ already exists.
#
# Options:
#
# +mode+:: Temporarily changes the API mode. See the MODES array.
#
# TODO The API documentation says this method returns 404/403 if the source or target is invalid, but it actually returns 5xx.
def move(source, target, options={})
source = source.sub(/^\//, '')
target = target.sub(/^\//, '')
target << File.basename(source) if target.ends_with?('/')
begin
parse_metadata(post('fileops', 'move', :from_path => Dropbox.check_path(source), :to_path => Dropbox.check_path(target), :root => root(options), :ssl => @ssl)).to_struct_recursively
rescue UnsuccessfulResponseError => error
raise FileNotFoundError.new(source) if error.response.kind_of?(Net::HTTPNotFound)
raise FileExistsError.new(target) if error.response.kind_of?(Net::HTTPForbidden)
raise error
end
end
alias :mv :move
# Renames a file. Takes the same options and raises the same exceptions as
# the move method.
#
# Calling
#
# session.rename 'path/to/file', 'new_name'
#
# is equivalent to calling
#
# session.move 'path/to/file', 'path/to/new_name'
def rename(path, new_name, options={})
raise ArgumentError, "Names cannot have slashes in them" if new_name.include?('/')
path = path.sub(/\/$/, '')
destination = path.split('/')
destination[destination.size - 1] = new_name
destination = destination.join('/')
move path, destination, options
end
# Returns a cookie-protected URL that the authorized user can use to view
# the file at the given path. This URL requires an authorized user.
#
# The path is assumed to be relative to the configured mode's root.
#
# Options:
#
# +mode+:: Temporarily changes the API mode. See the MODES array.
def link(path, options={})
path = path.sub(/^\//, '')
begin
rest = Dropbox.check_path(path).split('/')
rest << { :ssl => @ssl }
api_response(:get, 'links', root(options), *rest)
rescue UnsuccessfulResponseError => error
return error.response['Location'] if error.response.kind_of?(Net::HTTPFound)
#TODO shouldn't be using rescue blocks for normal program flow
raise error
end
end
memoize :link
# Creates and returns a shareable link to files or folders.
#
# The path is assumed to be relative to the configured mode's root.
#
# Options:
#
# +mode+:: Temporarily changes the API mode. See the MODES array.
def shares(path, options={})
path = path.sub(/^\//, '')
rest = Dropbox.check_path(path).split('/')
begin
return JSON.parse( api_response(:post, 'shares', root(options), *rest).body ).symbolize_keys_recursively
rescue UnsuccessfulResponseError => error
return error.response['Location'] if error.response.kind_of?(Net::HTTPFound)
#TODO shouldn't be using rescue blocks for normal program flow
raise error
end
end
memoize :shares
# Returns a +Struct+ containing metadata on a given file or folder. The path
# is assumed to be relative to the configured mode's root.
#
# If you pass a directory for +path+, the metadata will also contain a
# listing of the directory contents (unless the +suppress_list+ option is
# true).
#
# For information on the schema of the return struct, see the Dropbox API
# at https://www.dropbox.com/developers/docs#metadata
#
# The +modified+ key will be converted into a +Time+ instance. The +is_dir+
# key will also be available as <tt>directory?</tt>.
#
# Options:
#
# +suppress_list+:: Set this to true to remove the directory list from
# the result (only applicable if +path+ is a directory).
# +limit+:: Set this value to limit the number of entries returned when
# listing a directory. If the result has more than this number of
# entries, a TooManyEntriesError will be raised.
# +prior_response+:: The response from a prior call to metadata for the same
# path. If the metadata has not changed since the prior
# call, the entire metadata will not be re-downloaded.
# Operation is undefined if the given value was for a
# call to metadata with a different path.
# +mode+:: Temporarily changes the API mode. See the MODES array.
def metadata(path, options={})
path = path.sub(/^\//, '')
args = [
'metadata',
root(options)
]
args += Dropbox.check_path(path).split('/')
args << Hash.new
args.last[:file_limit] = options[:limit] if options[:limit]
args.last[:hash] = options[:prior_response].hash if options[:prior_response] and options[:prior_response].hash
args.last[:list] = !(options[:suppress_list].to_bool)
args.last[:ssl] = @ssl
begin
parse_metadata(get(*args)).to_struct_recursively
rescue UnsuccessfulResponseError => error
raise TooManyEntriesError.new(path) if error.response.kind_of?(Net::HTTPNotAcceptable)
raise FileNotFoundError.new(path) if error.response.kind_of?(Net::HTTPNotFound)
return options[:prior_response] if error.response.kind_of?(Net::HTTPNotModified)
raise error
end
end
memoize :metadata
alias :info :metadata
# Returns an array of <tt>Struct</tt>s with information on each file within
# the given directory. Calling
#
# session.list 'my/folder'
#
# is equivalent to calling
#
# session.metadata('my/folder').contents
#
# Returns nil if the path is not a directory. Raises the same exceptions as
# the metadata method. Takes the same options as the metadata method, except
# the +suppress_list+ option is implied to be false.
def list(path, options={})
metadata(path, options.merge(:suppress_list => false)).contents
end
alias :ls :list
def event_metadata(target_events, options={}) # :nodoc:
get 'event_metadata', :ssl => @ssl, :root => root(options), :target_events => target_events
end
def event_content(entry, options={}) # :nodoc:
request = Dropbox.api_url('event_content', :target_event => entry, :ssl => @ssl, :root => root(options))
response = api_internal(:get, request)
begin
return response.body, JSON.parse(response.header['X-Dropbox-Metadata'])
rescue JSON::ParserError
raise ParseError.new(request, response)
end
end
# Returns the configured API mode.
def mode
@api_mode ||= :sandbox
end
# Sets the API mode. See the MODES array.
def mode=(newmode)
raise ArgumentError, "Unknown API mode #{newmode.inspect}" unless MODES.include?(newmode)
@api_mode = newmode
end
private
def parse_metadata(hsh)
hsh[:modified] = Time.parse(hsh[:modified]) if hsh[:modified]
hsh[:directory?] = hsh[:is_dir]
hsh.each { |_,v| parse_metadata(v) if v.kind_of?(Hash) }
hsh.each { |_,v| v.each { |h| parse_metadata(h) if h.kind_of?(Hash) } if v.kind_of?(Array) }
hsh
end
def root(options={})
api_mode = options[:mode] || mode
raise ArgumentError, "Unknown API mode #{api_mode.inspect}" unless MODES.include?(api_mode)
return api_mode == :sandbox ? 'sandbox' : 'dropbox'
end
def get(*params)
api_json :get, *params
end
def post(*params)
api_json :post, *params
end
def api_internal(method, request)
raise UnauthorizedError, "Must authorize before you can use API method" unless @access_token
response = @access_token.send(method, request)
raise UnsuccessfulResponseError.new(request, response) unless response.kind_of?(Net::HTTPSuccess)
return response
end
def api_json(method, *params)
request = Dropbox.api_url(*params)
response = api_internal(method, request)
begin
return JSON.parse(response.body).symbolize_keys_recursively
rescue JSON::ParserError
raise ParseError.new(request, response)
end
end
def api_body(method, *params)
api_response(method, *params).body
end
def api_response(method, *params)
api_internal(method, Dropbox.api_url(*params))
end
end
# Superclass for exceptions raised when the server reports an error.
class APIError < StandardError
# The request URL.
attr_reader :request
# The Net::HTTPResponse returned by the server.
attr_reader :response
def initialize(request, response) # :nodoc:
@request = request
@response = response
end
def to_s # :nodoc:
"API error: #{request}"
end
end
# Raised when the Dropbox API returns a response that was not understood.
class ParseError < APIError
def to_s # :nodoc:
"Invalid response received: #{request}"
end
end
# Raised when something other than 200 OK is returned by an API method.
class UnsuccessfulResponseError < APIError
def to_s # :nodoc:
"HTTP status #{@response.class.to_s} received: #{request}"
end
end
# Superclass of errors relating to Dropbox files.
class FileError < StandardError
# The path of the offending file.
attr_reader :path
def initialize(path) # :nodoc:
@path = path
end
def to_s # :nodoc:
"#{self.class.to_s}: #{@path}"
end
end
# Raised when a Dropbox file doesn't exist.
class FileNotFoundError < FileError; end
# Raised when a Dropbox file is in the way.
class FileExistsError < FileError; end
# Raised when the number of files within a directory exceeds a specified
# limit.
class TooManyEntriesError < FileError; end
# Raised when the event_metadata method returns an error.
class PingbackError < StandardError
# The HTTP error code returned by the event_metadata method.
attr_reader :code
def initialize(code) # :nodoc
@code = code
end
def to_s # :nodoc:
"#{self.class.to_s} code #{@code}"
end
end
end