dropbox 0.0.10 → 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.
@@ -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