dropbox 0.0.10 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore CHANGED
@@ -1,4 +1,24 @@
1
- pkg/
1
+ ## MAC OS
2
2
  .DS_Store
3
- *.gem
4
- dropbox.gemspec
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## RUBYMINE
17
+ .idea
18
+
19
+ ## PROJECT::GENERAL
20
+ coverage
21
+ rdoc
22
+ pkg
23
+
24
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Tim Morgan
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -1,22 +1,89 @@
1
- = DropBox
1
+ = Ruby Dropbox Gem
2
2
 
3
- A simple stop-gap Ruby API for DropBox. Hopefully DropBox will release a real API soon!
3
+ An easy-to-use third-party interface to the RESTful Dropbox API.
4
4
 
5
- * Don't have a dropbox account? Well you should! You can "donate" to this project by signing up with this referral url: https://www.dropbox.com/referrals/NTI0MDI3MzU5
5
+ == Installation
6
6
 
7
- == INSTALL:
8
- * sudo gem install dropbox
7
+ sudo gem install rdropbox
9
8
 
10
- == USAGE:
9
+ == Tutorial by Example
11
10
 
12
- d = DropBox.new("you@email.com","password!") #login
13
- d.list("/") # show directory contents
14
- d.create_directory("/testdirectory") # create a remove directory
15
- d.create("/local/path/to/file.txt","/testdirectory") # upload a file to the directory
16
- d.rename("/testdirectory/file.txt","file.txt") # rename the file
17
- d.destroy("file.txt") # remove the file
18
- d.usage_stats
19
- # => {:regular_used=>18.6, :shared_used=>670.2, :free=>1359.2, :used=>688.8, :percent=>34%, :total=>2048.0}
20
-
21
- d2 = DropBox.new("you@email.com","password!", "optional_namespace") # all actions will happen in /optional_namespace
22
-
11
+ First things first: Be sure you've gotten a consumer key and secret from
12
+ http://developers.dropbox.com
13
+
14
+ # STEP 1: Authorize the user
15
+ session = Dropbox::Session.new('your_consumer_key', 'your_consumer_secret')
16
+ puts "Visit #{session.authorize_url} to log in to Dropbox. Hit enter when you have done this."
17
+ gets
18
+ session.authorize
19
+
20
+ # STEP 2: Play!
21
+ session.upload('testfile.txt')
22
+ uploaded_file = session.file('testfile.txt')
23
+ puts uploaded_file.metadata.size
24
+
25
+ uploaded_file.move 'new_name.txt'
26
+ uploaded_file.delete
27
+
28
+ == Tutorial by Example, Rails Edition
29
+
30
+ A simple Rails controller that allows a user to first authorize their Dropbox
31
+ account, and then upload a file to their Dropbox.
32
+
33
+ class DropboxController < ApplicationController
34
+ def authorize
35
+ if params[:oauth_token] then
36
+ dropbox_session = Dropbox::Session.deserialize(session[:dropbox_session])
37
+ dropbox_session.authorize(params)
38
+ session[:dropbox_session] = dropbox_session.serialize # re-serialize the authenticated session
39
+
40
+ redirect_to :action => 'upload'
41
+ else
42
+ dropbox_session = Dropbox::Session.new('your_consumer_key', 'your_consumer_secret')
43
+ session[:dropbox_session] = dropbox_session.serialize
44
+ redirect_to dropbox_session.authorize_url(:oauth_callback => url_for(:action => 'authorize'))
45
+ end
46
+ end
47
+
48
+ def upload
49
+ return redirect_to(:action => 'authorize') unless session[:dropbox_session]
50
+ dropbox_session = Dropbox::Session.deserialize(session[:dropbox_session])
51
+ return redirect_to(:action => 'authorize') unless dropbox_session.authorized?
52
+
53
+ if request.method == :post then
54
+ dropbox_session.upload params[:file], 'My Uploads'
55
+ render :text => 'Uploaded OK'
56
+ else
57
+ # display a multipart file field form
58
+ end
59
+ end
60
+ end
61
+
62
+ == Features and Where to Find Them
63
+
64
+ * Start with the Dropbox::Session class. The first thing you should do is
65
+ authenticate your users and that class is how to do it.
66
+ * The Dropbox::API module (attached to the Dropbox::Session class) is the meat
67
+ and potatoes. Use it to modify a user's Dropbox.
68
+ * The Dropbox::Entry class is a more object-oriented way of manipulating files.
69
+ It's totally optional; check it out if you like OOP.
70
+ * The Dropbox::Memoization module has some handy utility methods for memoizing
71
+ server responses to reduce network calls. It's plug-in compatible with any
72
+ caching strategy you might already have (memcache, etc.).
73
+ * If you're using pingbacks, check out Dropbox::Event and Dropbox::Revision.
74
+ Those classes parse pingbacks from Dropbox into Ruby objects.
75
+
76
+ == Note on Patches/Pull Requests
77
+
78
+ * Fork the project.
79
+ * Make your feature addition or bug fix.
80
+ * Add tests for it. This is important so I don't break it in a
81
+ future version unintentionally.
82
+ * Commit, do not mess with rakefile, version, or history.
83
+ (if you want to have your own version, that is fine but
84
+ bump version in a commit by itself I can ignore when I pull)
85
+ * Send me a pull request. Bonus points for topic branches.
86
+
87
+ == Copyright
88
+
89
+ Copyright (c) 2009 Tim Morgan. See LICENSE for details.
data/Rakefile CHANGED
@@ -1,35 +1,54 @@
1
1
  require 'rubygems'
2
2
  require 'rake'
3
- require 'spec/rake/spectask'
4
3
 
5
4
  begin
6
5
  require 'jeweler'
7
6
  Jeweler::Tasks.new do |gem|
8
7
  gem.name = "dropbox"
9
- open("VERSION") do |f|
10
- gem.version = f.read
11
- end
12
- gem.summary = %Q{A Simple DropBox API in Ruby}
13
- gem.description = %Q{A Simple DropBox API in Ruby}
14
- gem.email = "tys@tvg.ca"
15
- gem.homepage = "http://github.com/tvongaza/DropBox"
16
- gem.authors = ["Tys von Gaza","JP Hastings-Spital","Chris Searle","Nicholas A. Evans"]
17
- gem.add_development_dependency "Shoulda"
18
- gem.add_dependency "mechanize",'>= 1.0.0'
19
- gem.add_dependency "nokogiri", '>= 1.2.1'
20
- # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
8
+ gem.version = File.read("VERSION").chomp.strip
9
+ gem.summary = %Q{Ruby client library for the official Dropbox API}
10
+ gem.description = %Q{An easy-to-use client library for the official Dropbox API.}
11
+ gem.email = "dropbox@timothymorgan.info"
12
+ gem.homepage = "http://github.com/RISCfuture/dropbox"
13
+ gem.authors = ["Tim Morgan"]
14
+
15
+ gem.files += FileList["lib/dropbox/*.rb"]
16
+ gem.add_development_dependency "rspec", ">= 1.2.9"
17
+ gem.add_runtime_dependency "oauth", ">= 0.3.6"
18
+ gem.add_runtime_dependency "json", ">= 1.2.0"
19
+ gem.add_runtime_dependency "multipart-post", ">= 1.0"
20
+
21
+ gem.rubyforge_project = "dropbox"
21
22
  end
22
23
  Jeweler::GemcutterTasks.new
24
+ Jeweler::RubyforgeTasks.new
23
25
  rescue LoadError
24
26
  puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
25
27
  end
26
28
 
27
- desc "Run all examples"
28
- Spec::Rake::SpecTask.new('examples') do |t|
29
- t.spec_files = FileList['examples/**/*.rb']
30
- t.libs << 'lib'
31
- t.spec_opts = %w[-c -fs]
32
- t.ruby_opts = %w[-rubygems]
29
+ require 'spec/rake/spectask'
30
+ Spec::Rake::SpecTask.new(:spec) do |spec|
31
+ spec.libs << 'lib' << 'spec'
32
+ spec.spec_files = FileList['spec/**/*_spec.rb']
33
+ end
34
+
35
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
36
+ spec.libs << 'lib' << 'spec'
37
+ spec.pattern = 'spec/**/*_spec.rb'
38
+ spec.rcov = true
33
39
  end
34
40
 
35
- task :default => :examples
41
+ task :spec => :check_dependencies
42
+
43
+ task :default => :spec
44
+
45
+ require 'rake/rdoctask'
46
+ Rake::RDocTask.new do |rdoc|
47
+ version = File.exist?('VERSION') ? File.read('VERSION').chomp.strip : ""
48
+
49
+ rdoc.rdoc_dir = 'rdoc'
50
+ rdoc.title = "Dropbox API Client #{version}"
51
+ rdoc.rdoc_files.include('README*')
52
+ rdoc.rdoc_files.include('LICENSE')
53
+ rdoc.rdoc_files.include('lib/**/*.rb')
54
+ end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.10
1
+ 1.0.0
@@ -1,14 +1,43 @@
1
- require 'dropbox/dropbox'
2
-
3
- ## Should I include this?
4
- require 'delegate'
5
-
6
- # Allows percentages to be inspected and stringified in human
7
- # form "33.3%", but kept in a float format for mathmatics
8
- class Percentage < DelegateClass(Float)
9
- def to_s(decimalplaces = 0)
10
- (((self * 10**(decimalplaces+2)).round)/10**decimalplaces).to_s+"%"
1
+ # Defines the Dropbox module.
2
+
3
+ require 'cgi'
4
+ require 'yaml'
5
+ require 'digest/sha1'
6
+ require 'thread'
7
+ require 'set'
8
+ require 'time'
9
+ require 'tempfile'
10
+
11
+ Dir.glob("#{File.expand_path File.dirname(__FILE__)}/extensions/*.rb") { |file| require file }
12
+ Dir.glob("#{File.expand_path File.dirname(__FILE__)}/dropbox/*.rb") { |file| require file }
13
+
14
+ # Container module for the all Dropbox API classes.
15
+
16
+ module Dropbox
17
+ # The API version this client works with.
18
+ VERSION = "0"
19
+ # The host serving API requests.
20
+ HOST = "http://api.dropbox.com"
21
+ # The SSL host serving API requests.
22
+ SSL_HOST = "https://api.dropbox.com"
23
+ # Alternate hosts for other API requests.
24
+ ALTERNATE_HOSTS = { 'event_content' => 'http://api-content.dropbox.com', 'files' => "http://api-content.dropbox.com" }
25
+ # Alternate SSL hosts for other API requests.
26
+ ALTERNATE_SSL_HOSTS = { 'event_content' => 'https://api-content.dropbox.com', 'files' => "https://api-content.dropbox.com" }
27
+
28
+ def self.api_url(*paths_and_options) # :nodoc:
29
+ params = paths_and_options.extract_options!
30
+ ssl = params.delete(:ssl)
31
+ host = (ssl ? ALTERNATE_SSL_HOSTS[paths_and_options.first] : ALTERNATE_HOSTS[paths_and_options.first]) || (ssl ? SSL_HOST : HOST)
32
+ url = "#{host}/#{VERSION}/#{paths_and_options.map { |path_elem| CGI.escape path_elem.to_s }.join('/')}"
33
+ url.gsub! '+', '%20' # dropbox doesn't really like plusses
34
+ url << "?#{params.map { |k,v| CGI.escape(k.to_s) + "=" + CGI.escape(v.to_s) }.join('&')}" unless params.empty?
35
+ return url
11
36
  end
12
- alias :inspect :to_s
13
- end
14
37
 
38
+ def self.check_path(path) # :nodoc:
39
+ raise ArgumentError, "Backslashes are not allowed in Dropbox paths" if path.include?('\\')
40
+ raise ArgumentError, "Dropbox paths are limited to 256 characters in length" if path.size > 256
41
+ return path
42
+ end
43
+ end
@@ -0,0 +1,530 @@
1
+ # Defines the Dropbox::API module.
2
+
3
+ require "#{File.expand_path File.dirname(__FILE__)}/memoization"
4
+ require 'json'
5
+ require 'net/http/post/multipart'
6
+
7
+ module Dropbox
8
+
9
+ # Extensions to the Dropbox::Session class that add core Dropbox API
10
+ # functionality to this class. You must have authenticated your
11
+ # Dropbox::Session instance before you can call any of these methods. (See the
12
+ # Dropbox::Session class documentation for instructions.)
13
+ #
14
+ # API methods generally return +Struct+ objects containing their results,
15
+ # unless otherwise noted. See the Dropbox API documentation at
16
+ # http://developers.dropbox.com for specific information on the schema of each
17
+ # result.
18
+ #
19
+ # You can opt-in to memoization of API method results. See the
20
+ # Dropbox::Memoization class documentation to learn more.
21
+ #
22
+ # == Modes
23
+ #
24
+ # The Dropbox API works in three modes: sandbox, Dropbox (root), and
25
+ # metadata-only.
26
+ #
27
+ # * In sandbox mode (the default), all operations are rooted from your
28
+ # application's sandbox folder; other files elsewhere on the user's Dropbox
29
+ # are inaccessible.
30
+ # * In Dropbox mode, the root is the user's Dropbox folder, and all files are
31
+ # accessible. This mode is typically only available to certain API users.
32
+ # * In metadata-only mode, the root is the Dropbox folder, but write access
33
+ # is not available. Operations that modify the user's files will
34
+ # fail.
35
+ #
36
+ # You should configure the Dropbox::Session instance to use whichever mode
37
+ # you chose when you set up your application:
38
+ #
39
+ # session.mode = :metadata_only
40
+ #
41
+ # Valid values are listed in Dropbox::API::MODES, and this step is not
42
+ # necessary for sandboxed applications, as the sandbox mode is the default.
43
+ #
44
+ # You can also temporarily change the mode for many method calls using their
45
+ # options hash:
46
+ #
47
+ # session.move 'my_file', 'new/path', :mode => :dropbox
48
+
49
+ module API
50
+ include Dropbox::Memoization
51
+
52
+ # Valid API modes for the #mode= method.
53
+ MODES = [ :sandbox, :dropbox, :metadata_only ]
54
+
55
+ # Returns a Dropbox::Entry instance that can be used to work with files or
56
+ # directories in an object-oriented manner.
57
+
58
+ def entry(path)
59
+ Dropbox::Entry.new(self, path)
60
+ end
61
+ alias :file :entry
62
+ alias :directory :entry
63
+ alias :dir :entry
64
+
65
+ # Returns a +Struct+ with information about the user's account. See
66
+ # http://developers.dropbox.com/python/base.html#account-info for more
67
+ # information on the data returned.
68
+
69
+ def account
70
+ get('account', 'info', :ssl => @ssl).to_struct_recursively
71
+ end
72
+ memoize :account
73
+
74
+ # Downloads the file at the given path relative to the configured mode's
75
+ # root.
76
+ #
77
+ # Returns the contents of the downloaded file as a +String+. Support for
78
+ # streaming downloads and range queries is available server-side, but not
79
+ # available in this API client due to limitations of the OAuth gem.
80
+ #
81
+ # Options:
82
+ #
83
+ # +mode+:: Temporarily changes the API mode. See the MODES array.
84
+
85
+ def download(path, options={})
86
+ path.sub! /^\//, ''
87
+ rest = Dropbox.check_path(path).split('/')
88
+ rest << { :ssl => @ssl }
89
+ api_body :get, 'files', root(options), *rest
90
+ #TODO streaming, range queries
91
+ end
92
+
93
+ # Uploads a file to a path relative to the configured mode's root. The
94
+ # +remote_path+ parameter is taken to be the path portion _only_; the name
95
+ # of the remote file will be identical to that of the local file. You can
96
+ # provide any of the following for the first parameter:
97
+ #
98
+ # * a +File+ object, in which case the name of the local file is used, or
99
+ # * a path to a file, in which case that file's name is used.
100
+ #
101
+ # Options:
102
+ #
103
+ # +mode+:: Temporarily changes the API mode. See the MODES array.
104
+ #
105
+ # Examples:
106
+ #
107
+ # session.upload 'music.pdf', '/' # upload a file by path to the root directory
108
+ # session.upload 'music.pdf, 'music/' # upload a file by path to the music folder
109
+ # session.upload File.new('music.pdf'), '/' # same as the first example
110
+
111
+ def upload(local_file, remote_path, options={})
112
+ if local_file.kind_of?(File) or local_file.kind_of?(Tempfile) then
113
+ file = local_file
114
+ name = local_file.respond_to?(:original_filename) ? local_file.original_filename : File.basename(local_file.path)
115
+ local_path = local_file.path
116
+ elsif local_file.kind_of?(String) then
117
+ file = File.new(local_file)
118
+ name = File.basename(local_file)
119
+ local_path = local_file
120
+ else
121
+ raise ArgumentError, "local_file must be a File or file path"
122
+ end
123
+
124
+ remote_path.sub! /^\//, ''
125
+ remote_path = Dropbox.check_path(remote_path).split('/')
126
+
127
+ remote_path << { :ssl => @ssl }
128
+ url = Dropbox.api_url('files', root(options), *remote_path)
129
+ uri = URI.parse(url)
130
+
131
+ oauth_request = Net::HTTP::Post.new(uri.path)
132
+ oauth_request.set_form_data 'file' => name
133
+
134
+ alternate_host_session = clone_with_host(@ssl ? Dropbox::ALTERNATE_SSL_HOSTS['files'] : Dropbox::ALTERNATE_HOSTS['files'])
135
+ alternate_host_session.instance_variable_get(:@consumer).sign!(oauth_request, @access_token)
136
+ oauth_signature = oauth_request.to_hash['authorization']
137
+
138
+ request = Net::HTTP::Post::Multipart.new(uri.path,
139
+ 'file' => UploadIO.convert!(
140
+ file,
141
+ 'application/octet-stream',
142
+ name,
143
+ local_path))
144
+ request['authorization'] = oauth_signature.join(', ')
145
+
146
+ response = Net::HTTP.start(uri.host, uri.port) { |http| http.request(request) }
147
+ if response.kind_of?(Net::HTTPSuccess) then
148
+ begin
149
+ return JSON.parse(response.body).symbolize_keys_recursively.to_struct_recursively
150
+ rescue JSON::ParserError
151
+ raise ParseError.new(uri.to_s, response)
152
+ end
153
+ else
154
+ raise UnsuccessfulResponseError.new(uri.to_s, response)
155
+ end
156
+ end
157
+
158
+ # Copies the +source+ file to the path at +target+. If +target+ ends with a
159
+ # slash, the new file will share the same name as the old file. Returns a
160
+ # +Struct+ with metadata for the new file. (See the metadata method.)
161
+ #
162
+ # Both paths are assumed to be relative to the configured mode's root.
163
+ #
164
+ # Raises FileNotFoundError if +source+ does not exist. Raises
165
+ # FileExistsError if +target+ already exists.
166
+ #
167
+ # Options:
168
+ #
169
+ # +mode+:: Temporarily changes the API mode. See the MODES array.
170
+ #
171
+ # TODO The API documentation says this method returns 404/403 if the source or target is invalid, but it actually returns 5xx.
172
+
173
+ def copy(source, target, options={})
174
+ source.sub! /^\//, ''
175
+ target.sub! /^\//, ''
176
+ target << File.basename(source) if target.ends_with?('/')
177
+ begin
178
+ 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
179
+ rescue UnsuccessfulResponseError => error
180
+ raise FileNotFoundError.new(source) if error.response.kind_of?(Net::HTTPNotFound)
181
+ raise FileExistsError.new(target) if error.response.kind_of?(Net::HTTPForbidden)
182
+ raise error
183
+ end
184
+ end
185
+ alias :cp :copy
186
+
187
+ # Creates a folder at the given path. The path is assumed to be relative to
188
+ # the configured mode's root. Returns a +Struct+ with metadata about the new
189
+ # folder. (See the metadata method.)
190
+ #
191
+ # Raises FileExistsError if there is already a file or folder at +path+.
192
+ #
193
+ # Options:
194
+ #
195
+ # +mode+:: Temporarily changes the API mode. See the MODES array.
196
+ #
197
+ # 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.
198
+
199
+ def create_folder(path, options={})
200
+ path.sub! /^\//, ''
201
+ path.sub! /\/$/, ''
202
+ begin
203
+ parse_metadata(post('fileops', 'create_folder', :path => Dropbox.check_path(path), :root => root(options), :ssl => @ssl)).to_struct_recursively
204
+ rescue UnsuccessfulResponseError => error
205
+ raise FileExistsError.new(path) if error.response.kind_of?(Net::HTTPForbidden)
206
+ raise error
207
+ end
208
+ end
209
+ alias :mkdir :create_folder
210
+
211
+ # Deletes a file or folder at the given path. The path is assumed to be
212
+ # relative to the configured mode's root.
213
+ #
214
+ # Raises FileNotFoundError if the file or folder does not exist at +path+.
215
+ #
216
+ # Options:
217
+ #
218
+ # +mode+:: Temporarily changes the API mode. See the MODES array.
219
+ #
220
+ # TODO The API documentation says this method returns 404 if the path does not exist, but it actually returns 5xx.
221
+
222
+ def delete(path, options={})
223
+ path.sub! /^\//, ''
224
+ path.sub! /\/$/, ''
225
+ begin
226
+ api_response(:post, 'fileops', 'delete', :path => Dropbox.check_path(path), :root => root(options), :ssl => @ssl)
227
+ rescue UnsuccessfulResponseError => error
228
+ raise FileNotFoundError.new(path) if error.response.kind_of?(Net::HTTPNotFound)
229
+ raise error
230
+ end
231
+ return true
232
+ end
233
+ alias :rm :delete
234
+
235
+ # Moves the +source+ file to the path at +target+. If +target+ ends with a
236
+ # slash, the file name will remain unchanged. If +source+ and +target+ share
237
+ # the same path but have differing file names, the file will be renamed (see
238
+ # also the rename method). Returns a +Struct+ with metadata for the new
239
+ # file. (See the metadata method.)
240
+ #
241
+ # Both paths are assumed to be relative to the configured mode's root.
242
+ #
243
+ # Raises FileNotFoundError if +source+ does not exist. Raises
244
+ # FileExistsError if +target+ already exists.
245
+ #
246
+ # Options:
247
+ #
248
+ # +mode+:: Temporarily changes the API mode. See the MODES array.
249
+ #
250
+ # TODO The API documentation says this method returns 404/403 if the source or target is invalid, but it actually returns 5xx.
251
+
252
+ def move(source, target, options={})
253
+ source.sub! /^\//, ''
254
+ target.sub! /^\//, ''
255
+ target << File.basename(source) if target.ends_with?('/')
256
+ begin
257
+ 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
258
+ rescue UnsuccessfulResponseError => error
259
+ raise FileNotFoundError.new(source) if error.response.kind_of?(Net::HTTPNotFound)
260
+ raise FileExistsError.new(target) if error.response.kind_of?(Net::HTTPForbidden)
261
+ raise error
262
+ end
263
+ end
264
+ alias :mv :move
265
+
266
+ # Renames a file. Takes the same options and raises the same exceptions as
267
+ # the move method.
268
+ #
269
+ # Calling
270
+ #
271
+ # session.rename 'path/to/file', 'new_name'
272
+ #
273
+ # is equivalent to calling
274
+ #
275
+ # session.move 'path/to/file', 'path/to/new_name'
276
+
277
+ def rename(path, new_name, options={})
278
+ raise ArgumentError, "Names cannot have slashes in them" if new_name.include?('/')
279
+ path.sub! /\/$/, ''
280
+ destination = path.split('/')
281
+ destination[destination.size - 1] = new_name
282
+ destination = destination.join('/')
283
+ move path, destination, options
284
+ end
285
+
286
+ # Returns a cookie-protected URL that the authorized user can use to view
287
+ # the file at the given path. This URL requires an authorized user.
288
+ #
289
+ # The path is assumed to be relative to the configured mode's root.
290
+ #
291
+ # Options:
292
+ #
293
+ # +mode+:: Temporarily changes the API mode. See the MODES array.
294
+
295
+ def link(path, options={})
296
+ path.sub! /^\//, ''
297
+ begin
298
+ rest = Dropbox.check_path(path).split('/')
299
+ rest << { :ssl => @ssl }
300
+ api_response(:get, 'links', root(options), *rest)
301
+ rescue UnsuccessfulResponseError => error
302
+ return error.response['Location'] if error.response.kind_of?(Net::HTTPFound)
303
+ #TODO shouldn't be using rescue blocks for normal program flow
304
+ raise error
305
+ end
306
+ end
307
+ memoize :link
308
+
309
+ # Returns a +Struct+ containing metadata on a given file or folder. The path
310
+ # is assumed to be relative to the configured mode's root.
311
+ #
312
+ # If you pass a directory for +path+, the metadata will also contain a
313
+ # listing of the directory contents (unless the +suppress_list+ option is
314
+ # true).
315
+ #
316
+ # For information on the schema of the return struct, see the Dropbox API
317
+ # at http://developers.dropbox.com/python/base.html#metadata
318
+ #
319
+ # The +modified+ key will be converted into a +Time+ instance. The +is_dir+
320
+ # key will also be available as <tt>directory?</tt>.
321
+ #
322
+ # Options:
323
+ #
324
+ # +suppress_list+:: Set this to true to remove the directory list from
325
+ # the result (only applicable if +path+ is a directory).
326
+ # +limit+:: Set this value to limit the number of entries returned when
327
+ # listing a directory. If the result has more than this number of
328
+ # entries, a TooManyEntriesError will be raised.
329
+ # +mode+:: Temporarily changes the API mode. See the MODES array.
330
+ #
331
+ # TODO hash option seems to return HTTPBadRequest for now
332
+
333
+ def metadata(path, options={})
334
+ path.sub! /^\//, ''
335
+ args = [
336
+ 'metadata',
337
+ root(options)
338
+ ]
339
+ args += Dropbox.check_path(path).split('/')
340
+ args << Hash.new
341
+ args.last[:file_limit] = options[:limit] if options[:limit]
342
+ #args.last[:hash] = options[:hash] if options[:hash]
343
+ args.last[:list] = !(options[:suppress_list].to_bool)
344
+ args.last[:ssl] = @ssl
345
+
346
+ begin
347
+ parse_metadata(get(*args)).to_struct_recursively
348
+ rescue UnsuccessfulResponseError => error
349
+ raise TooManyEntriesError.new(path) if error.response.kind_of?(Net::HTTPNotAcceptable)
350
+ raise FileNotFoundError.new(path) if error.response.kind_of?(Net::HTTPNotFound)
351
+ #return :not_modified if error.kind_of?(Net::HTTPNotModified)
352
+ raise error
353
+ end
354
+ end
355
+ memoize :metadata
356
+ alias :info :metadata
357
+
358
+ # Returns an array of <tt>Struct</tt>s with information on each file within
359
+ # the given directory. Calling
360
+ #
361
+ # session.list 'my/folder'
362
+ #
363
+ # is equivalent to calling
364
+ #
365
+ # session.metadata('my/folder').contents
366
+ #
367
+ # Returns nil if the path is not a directory. Raises the same exceptions as
368
+ # the metadata method. Takes the same options as the metadata method, except
369
+ # the +suppress_list+ option is implied to be false.
370
+
371
+
372
+ def list(path, options={})
373
+ metadata(path, options.merge(:suppress_list => false)).contents
374
+ end
375
+ alias :ls :list
376
+
377
+ def event_metadata(target_events, options={}) # :nodoc:
378
+ get 'event_metadata', :ssl => @ssl, :root => root(options), :target_events => target_events
379
+ end
380
+
381
+ def event_content(entry, options={}) # :nodoc:
382
+ request = Dropbox.api_url('event_content', :target_event => entry, :ssl => @ssl, :root => root(options))
383
+ response = api_internal(:get, request)
384
+ begin
385
+ return response.body, JSON.parse(response.header['X-Dropbox-Metadata'])
386
+ rescue JSON::ParserError
387
+ raise ParseError.new(request, response)
388
+ end
389
+ end
390
+
391
+ # Returns the configured API mode.
392
+
393
+ def mode
394
+ @api_mode ||= :sandbox
395
+ end
396
+
397
+ # Sets the API mode. See the MODES array.
398
+
399
+ def mode=(newmode)
400
+ raise ArgumentError, "Unknown API mode #{newmode.inspect}" unless MODES.include?(newmode)
401
+ @api_mode = newmode
402
+ end
403
+
404
+ private
405
+
406
+ def parse_metadata(hsh)
407
+ hsh[:modified] = Time.parse(hsh[:modified]) if hsh[:modified]
408
+ hsh[:directory?] = hsh[:is_dir]
409
+ hsh.each { |_,v| parse_metadata(v) if v.kind_of?(Hash) }
410
+ hsh.each { |_,v| v.each { |h| parse_metadata(h) if h.kind_of?(Hash) } if v.kind_of?(Array) }
411
+ hsh
412
+ end
413
+
414
+ def root(options={})
415
+ api_mode = options[:mode] || mode
416
+ raise ArgumentError, "Unknown API mode #{api_mode.inspect}" unless MODES.include?(api_mode)
417
+ return api_mode == :sandbox ? 'sandbox' : 'dropbox'
418
+ end
419
+
420
+ def get(*params)
421
+ api_json :get, *params
422
+ end
423
+
424
+ def post(*params)
425
+ api_json :post, *params
426
+ end
427
+
428
+ def api_internal(method, request)
429
+ raise UnauthorizedError, "Must authorize before you can use API method" unless @access_token
430
+ response = @access_token.send(method, request)
431
+ raise UnsuccessfulResponseError.new(request, response) unless response.kind_of?(Net::HTTPSuccess)
432
+ return response
433
+ end
434
+
435
+ def api_json(method, *params)
436
+ request = Dropbox.api_url(*params)
437
+ response = api_internal(method, request)
438
+ begin
439
+ return JSON.parse(response.body).symbolize_keys_recursively
440
+ rescue JSON::ParserError
441
+ raise ParseError.new(request, response)
442
+ end
443
+ end
444
+
445
+ def api_body(method, *params)
446
+ api_response(method, *params).body
447
+ end
448
+
449
+ def api_response(method, *params)
450
+ api_internal(method, Dropbox.api_url(*params))
451
+ end
452
+ end
453
+
454
+ # Superclass for exceptions raised when the server reports an error.
455
+
456
+ class APIError < StandardError
457
+ # The request URL.
458
+ attr_reader :request
459
+ # The Net::HTTPResponse returned by the server.
460
+ attr_reader :response
461
+
462
+ def initialize(request, response) # :nodoc:
463
+ @request = request
464
+ @response = response
465
+ end
466
+
467
+ def to_s # :nodoc:
468
+ "API error: #{request}"
469
+ end
470
+ end
471
+
472
+ # Raised when the Dropbox API returns a response that was not understood.
473
+
474
+ class ParseError < APIError
475
+ def to_s # :nodoc:
476
+ "Invalid response received: #{request}"
477
+ end
478
+ end
479
+
480
+ # Raised when something other than 200 OK is returned by an API method.
481
+
482
+ class UnsuccessfulResponseError < APIError
483
+ def to_s # :nodoc:
484
+ "HTTP status #{@response.class.to_s} received: #{request}"
485
+ end
486
+ end
487
+
488
+ # Superclass of errors relating to Dropbox files.
489
+
490
+ class FileError < StandardError
491
+ # The path of the offending file.
492
+ attr_reader :path
493
+
494
+ def initialize(path) # :nodoc:
495
+ @path = path
496
+ end
497
+
498
+ def to_s # :nodoc:
499
+ "#{self.class.to_s}: #{@path}"
500
+ end
501
+ end
502
+
503
+ # Raised when a Dropbox file doesn't exist.
504
+
505
+ class FileNotFoundError < FileError; end
506
+
507
+ # Raised when a Dropbox file is in the way.
508
+
509
+ class FileExistsError < FileError; end
510
+
511
+ # Raised when the number of files within a directory exceeds a specified
512
+ # limit.
513
+
514
+ class TooManyEntriesError < FileError; end
515
+
516
+ # Raised when the event_metadata method returns an error.
517
+
518
+ class PingbackError < StandardError
519
+ # The HTTP error code returned by the event_metadata method.
520
+ attr_reader :code
521
+
522
+ def initialize(code) # :nodoc
523
+ @code = code
524
+ end
525
+
526
+ def to_s # :nodoc:
527
+ "#{self.class.to_s} code #{@code}"
528
+ end
529
+ end
530
+ end