dropbox-aliix 1.1.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.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,25 @@
1
+ ## MAC OS
2
+ .DS_Store
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
+ .rvmrc
24
+
25
+ ## PROJECT::SPECIFIC
data/ChangeLog ADDED
@@ -0,0 +1,10 @@
1
+ 1.1.0 (2010-05-27)
2
+
3
+ * Added thumbnails API method.
4
+ * Changed name of gem from rdropbox to dropbox.
5
+ * Fixed potential dependency issues.
6
+ * Documentation updates.
7
+
8
+ 1.0.0 (2010-05-05)
9
+
10
+ * Initial release.
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.
data/README.rdoc ADDED
@@ -0,0 +1,89 @@
1
+ = Ruby Dropbox Gem
2
+
3
+ An easy-to-use third-party interface to the RESTful Dropbox API.
4
+
5
+ == Installation
6
+
7
+ gem install dropbox
8
+
9
+ == Tutorial by Example
10
+
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 ADDED
@@ -0,0 +1,52 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "dropbox-aliix"
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
+ end
21
+ Jeweler::GemcutterTasks.new
22
+ Jeweler::RubyforgeTasks.new
23
+ rescue LoadError
24
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
25
+ end
26
+
27
+ require 'spec/rake/spectask'
28
+ Spec::Rake::SpecTask.new(:spec) do |spec|
29
+ spec.libs << 'lib' << 'spec'
30
+ spec.spec_files = FileList['spec/**/*_spec.rb']
31
+ end
32
+
33
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
34
+ spec.libs << 'lib' << 'spec'
35
+ spec.pattern = 'spec/**/*_spec.rb'
36
+ spec.rcov = true
37
+ end
38
+
39
+ task :spec => :check_dependencies
40
+
41
+ task :default => :spec
42
+
43
+ require 'rake/rdoctask'
44
+ Rake::RDocTask.new do |rdoc|
45
+ version = File.exist?('VERSION') ? File.read('VERSION').chomp.strip : ""
46
+
47
+ rdoc.rdoc_dir = 'rdoc'
48
+ rdoc.title = "Dropbox API Client #{version}"
49
+ rdoc.rdoc_files.include('README*')
50
+ rdoc.rdoc_files.include('LICENSE')
51
+ rdoc.rdoc_files.include('lib/**/*.rb')
52
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.1.0
data/dropbox.gemspec ADDED
@@ -0,0 +1,88 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{dropbox}
8
+ s.version = "1.1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Tim Morgan"]
12
+ s.date = %q{2010-09-03}
13
+ s.description = %q{An easy-to-use client library for the official Dropbox API.}
14
+ s.email = %q{dropbox@timothymorgan.info}
15
+ s.extra_rdoc_files = [
16
+ "ChangeLog",
17
+ "LICENSE",
18
+ "README.rdoc"
19
+ ]
20
+ s.files = [
21
+ ".document",
22
+ ".gitignore",
23
+ "ChangeLog",
24
+ "LICENSE",
25
+ "README.rdoc",
26
+ "Rakefile",
27
+ "VERSION",
28
+ "dropbox.gemspec",
29
+ "lib/dropbox.rb",
30
+ "lib/dropbox/api.rb",
31
+ "lib/dropbox/entry.rb",
32
+ "lib/dropbox/event.rb",
33
+ "lib/dropbox/extensions/array.rb",
34
+ "lib/dropbox/extensions/hash.rb",
35
+ "lib/dropbox/extensions/module.rb",
36
+ "lib/dropbox/extensions/object.rb",
37
+ "lib/dropbox/extensions/string.rb",
38
+ "lib/dropbox/extensions/to_bool.rb",
39
+ "lib/dropbox/memoization.rb",
40
+ "lib/dropbox/revision.rb",
41
+ "lib/dropbox/session.rb",
42
+ "spec/dropbox/api_spec.rb",
43
+ "spec/dropbox/entry_spec.rb",
44
+ "spec/dropbox/event_spec.rb",
45
+ "spec/dropbox/revision_spec.rb",
46
+ "spec/dropbox/session_spec.rb",
47
+ "spec/dropbox_spec.rb",
48
+ "spec/spec.opts",
49
+ "spec/spec_helper.rb"
50
+ ]
51
+ s.homepage = %q{http://github.com/RISCfuture/dropbox}
52
+ s.rdoc_options = ["--charset=UTF-8"]
53
+ s.require_paths = ["lib"]
54
+ s.rubygems_version = %q{1.3.7}
55
+ s.summary = %q{Ruby client library for the official Dropbox API}
56
+ s.test_files = [
57
+ "spec/dropbox/api_spec.rb",
58
+ "spec/dropbox/entry_spec.rb",
59
+ "spec/dropbox/event_spec.rb",
60
+ "spec/dropbox/revision_spec.rb",
61
+ "spec/dropbox/session_spec.rb",
62
+ "spec/dropbox_spec.rb",
63
+ "spec/spec_helper.rb"
64
+ ]
65
+
66
+ if s.respond_to? :specification_version then
67
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
68
+ s.specification_version = 3
69
+
70
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
71
+ s.add_development_dependency(%q<rspec>, [">= 1.2.9"])
72
+ s.add_runtime_dependency(%q<oauth>, [">= 0.3.6"])
73
+ s.add_runtime_dependency(%q<json>, [">= 1.2.0"])
74
+ s.add_runtime_dependency(%q<multipart-post>, [">= 1.0"])
75
+ else
76
+ s.add_dependency(%q<rspec>, [">= 1.2.9"])
77
+ s.add_dependency(%q<oauth>, [">= 0.3.6"])
78
+ s.add_dependency(%q<json>, [">= 1.2.0"])
79
+ s.add_dependency(%q<multipart-post>, [">= 1.0"])
80
+ end
81
+ else
82
+ s.add_dependency(%q<rspec>, [">= 1.2.9"])
83
+ s.add_dependency(%q<oauth>, [">= 0.3.6"])
84
+ s.add_dependency(%q<json>, [">= 1.2.0"])
85
+ s.add_dependency(%q<multipart-post>, [">= 1.0"])
86
+ end
87
+ end
88
+
data/lib/dropbox.rb ADDED
@@ -0,0 +1,62 @@
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
+ require 'dropbox/extensions/array'
12
+ require 'dropbox/extensions/hash'
13
+ require 'dropbox/extensions/module'
14
+ require 'dropbox/extensions/object'
15
+ require 'dropbox/extensions/string'
16
+ require 'dropbox/extensions/to_bool'
17
+
18
+ require 'dropbox/memoization'
19
+ require 'dropbox/api'
20
+ require 'dropbox/entry'
21
+ require 'dropbox/event'
22
+ require 'dropbox/revision'
23
+ require 'dropbox/session'
24
+
25
+ # Container module for the all Dropbox API classes.
26
+
27
+ module Dropbox
28
+ # The API version this client works with.
29
+ VERSION = "0"
30
+ # The host serving API requests.
31
+ HOST = "http://api.dropbox.com"
32
+ # The SSL host serving API requests.
33
+ SSL_HOST = "https://api.dropbox.com"
34
+ # Alternate hosts for other API requests.
35
+ ALTERNATE_HOSTS = {
36
+ 'event_content' => 'http://api-content.dropbox.com',
37
+ 'files' => 'http://api-content.dropbox.com',
38
+ 'thumbnails' => 'http://api-content.dropbox.com'
39
+ }
40
+ # Alternate SSL hosts for other API requests.
41
+ ALTERNATE_SSL_HOSTS = {
42
+ 'event_content' => 'https://api-content.dropbox.com',
43
+ 'files' => 'https://api-content.dropbox.com',
44
+ 'thumbnails' => 'https://api-content.dropbox.com'
45
+ }
46
+
47
+ def self.api_url(*paths_and_options) # :nodoc:
48
+ params = paths_and_options.extract_options!
49
+ ssl = params.delete(:ssl)
50
+ host = (ssl ? ALTERNATE_SSL_HOSTS[paths_and_options.first] : ALTERNATE_HOSTS[paths_and_options.first]) || (ssl ? SSL_HOST : HOST)
51
+ url = "#{host}/#{VERSION}/#{paths_and_options.map { |path_elem| CGI.escape path_elem.to_s }.join('/')}"
52
+ url.gsub! '+', '%20' # dropbox doesn't really like plusses
53
+ url << "?#{params.map { |k,v| CGI.escape(k.to_s) + "=" + CGI.escape(v.to_s) }.join('&')}" unless params.empty?
54
+ return url
55
+ end
56
+
57
+ def self.check_path(path) # :nodoc:
58
+ raise ArgumentError, "Backslashes are not allowed in Dropbox paths" if path.include?('\\')
59
+ raise ArgumentError, "Dropbox paths are limited to 256 characters in length" if path.size > 256
60
+ return path
61
+ end
62
+ end
@@ -0,0 +1,576 @@
1
+ # Defines the Dropbox::API module.
2
+
3
+ require 'json'
4
+ require 'net/http/post/multipart'
5
+
6
+ module Dropbox
7
+
8
+ # Extensions to the Dropbox::Session class that add core Dropbox API
9
+ # functionality to this class. You must have authenticated your
10
+ # Dropbox::Session instance before you can call any of these methods. (See the
11
+ # Dropbox::Session class documentation for instructions.)
12
+ #
13
+ # API methods generally return +Struct+ objects containing their results,
14
+ # unless otherwise noted. See the Dropbox API documentation at
15
+ # http://developers.dropbox.com for specific information on the schema of each
16
+ # result.
17
+ #
18
+ # You can opt-in to memoization of API method results. See the
19
+ # Dropbox::Memoization class documentation to learn more.
20
+ #
21
+ # == Modes
22
+ #
23
+ # The Dropbox API works in three modes: sandbox, Dropbox (root), and
24
+ # metadata-only.
25
+ #
26
+ # * In sandbox mode (the default), all operations are rooted from your
27
+ # application's sandbox folder; other files elsewhere on the user's Dropbox
28
+ # are inaccessible.
29
+ # * In Dropbox mode, the root is the user's Dropbox folder, and all files are
30
+ # accessible. This mode is typically only available to certain API users.
31
+ # * In metadata-only mode, the root is the Dropbox folder, but write access
32
+ # is not available. Operations that modify the user's files will
33
+ # fail.
34
+ #
35
+ # You should configure the Dropbox::Session instance to use whichever mode
36
+ # you chose when you set up your application:
37
+ #
38
+ # session.mode = :metadata_only
39
+ #
40
+ # Valid values are listed in Dropbox::API::MODES, and this step is not
41
+ # necessary for sandboxed applications, as the sandbox mode is the default.
42
+ #
43
+ # You can also temporarily change the mode for many method calls using their
44
+ # options hash:
45
+ #
46
+ # session.move 'my_file', 'new/path', :mode => :dropbox
47
+
48
+ module API
49
+ include Dropbox::Memoization
50
+
51
+ # Valid API modes for the #mode= method.
52
+ MODES = [ :sandbox, :dropbox, :metadata_only ]
53
+
54
+ # Returns a Dropbox::Entry instance that can be used to work with files or
55
+ # directories in an object-oriented manner.
56
+
57
+ def entry(path)
58
+ Dropbox::Entry.new(self, path)
59
+ end
60
+ alias :file :entry
61
+ alias :directory :entry
62
+ alias :dir :entry
63
+
64
+ # Returns a +Struct+ with information about the user's account. See
65
+ # https://www.dropbox.com/developers/docs#account-info for more information
66
+ # on the data returned.
67
+
68
+ def account
69
+ get('account', 'info', :ssl => @ssl).to_struct_recursively
70
+ end
71
+ memoize :account
72
+
73
+ # Downloads the file at the given path relative to the configured mode's
74
+ # root.
75
+ #
76
+ # Returns the contents of the downloaded file as a +String+. Support for
77
+ # streaming downloads and range queries is available server-side, but not
78
+ # available in this API client due to limitations of the OAuth gem.
79
+ #
80
+ # Options:
81
+ #
82
+ # +mode+:: Temporarily changes the API mode. See the MODES array.
83
+
84
+ def download(path, options={})
85
+ path.sub! /^\//, ''
86
+ rest = Dropbox.check_path(path).split('/')
87
+ rest << { :ssl => @ssl }
88
+ api_body :get, 'files', root(options), *rest
89
+ #TODO streaming, range queries
90
+ end
91
+
92
+ # Downloads a minimized thumbnail for a file. Pass the path to the file,
93
+ # optionally the size of the thumbnail you want, and any additional options.
94
+ # See https://www.dropbox.com/developers/docs#thumbnails for a list of valid
95
+ # size specifiers.
96
+ #
97
+ # Returns the content of the thumbnail image as a +String+. The thumbnail
98
+ # data is in JPEG format. Returns +nil+ if the file does not have a
99
+ # thumbnail. You can check if a file has a thumbnail using the metadata
100
+ # method.
101
+ #
102
+ # Because of the way this API method works, if you pass in the name of a
103
+ # file that does not exist, you will not receive a 404, but instead just get
104
+ # +nil+.
105
+ #
106
+ # Options:
107
+ #
108
+ # +mode+:: Temporarily changes the API mode. See the MODES array.
109
+ #
110
+ # Examples:
111
+ #
112
+ # Get the thumbnail for an image (default thunmbnail size):
113
+ #
114
+ # session.thumbnail('my/image.jpg')
115
+ #
116
+ # Get the thumbnail for an image in the +medium+ size:
117
+ #
118
+ # session.thumbnail('my/image.jpg', 'medium')
119
+
120
+ def thumbnail(*args)
121
+ options = args.extract_options!
122
+ path = args.shift
123
+ size = args.shift
124
+ raise ArgumentError, "thumbnail takes a path, an optional size, and optional options" unless path.kind_of?(String) and (size.kind_of?(String) or size.nil?) and args.empty?
125
+
126
+ path.sub! /^\//, ''
127
+ rest = Dropbox.check_path(path).split('/')
128
+ rest << { :ssl => @ssl }
129
+ rest.last[:size] = size if size
130
+
131
+ begin
132
+ api_body :get, 'thumbnails', root(options), *rest
133
+ rescue Dropbox::UnsuccessfulResponseError => e
134
+ raise unless e.response.code.to_i == 404
135
+ return nil
136
+ end
137
+ end
138
+
139
+ # Uploads a file to a path relative to the configured mode's root. The
140
+ # +remote_path+ parameter is taken to be the path portion _only_; the name
141
+ # of the remote file will be identical to that of the local file. You can
142
+ # provide any of the following for the first parameter:
143
+ #
144
+ # * a +File+ object, in which case the name of the local file is used, or
145
+ # * a path to a file, in which case that file's name is used.
146
+ #
147
+ # Options:
148
+ #
149
+ # +mode+:: Temporarily changes the API mode. See the MODES array.
150
+ #
151
+ # Examples:
152
+ #
153
+ # session.upload 'music.pdf', '/' # upload a file by path to the root directory
154
+ # session.upload 'music.pdf, 'music/' # upload a file by path to the music folder
155
+ # session.upload File.new('music.pdf'), '/' # same as the first example
156
+
157
+ def upload(local_file, remote_path, options={})
158
+ if local_file.kind_of?(File) or local_file.kind_of?(Tempfile) then
159
+ file = local_file
160
+ name = local_file.respond_to?(:original_filename) ? local_file.original_filename : File.basename(local_file.path)
161
+ local_path = local_file.path
162
+ elsif local_file.kind_of?(String) then
163
+ file = File.new(local_file)
164
+ name = File.basename(local_file)
165
+ local_path = local_file
166
+ else
167
+ raise ArgumentError, "local_file must be a File or file path"
168
+ end
169
+
170
+ remote_path.sub! /^\//, ''
171
+ remote_path = Dropbox.check_path(remote_path).split('/')
172
+
173
+ remote_path << { :ssl => @ssl }
174
+ url = Dropbox.api_url('files', root(options), *remote_path)
175
+ uri = URI.parse(url)
176
+
177
+ oauth_request = Net::HTTP::Post.new(uri.path)
178
+ oauth_request.set_form_data 'file' => name
179
+
180
+ alternate_host_session = clone_with_host(@ssl ? Dropbox::ALTERNATE_SSL_HOSTS['files'] : Dropbox::ALTERNATE_HOSTS['files'])
181
+ alternate_host_session.instance_variable_get(:@consumer).sign!(oauth_request, @access_token)
182
+ oauth_signature = oauth_request.to_hash['authorization']
183
+
184
+ request = Net::HTTP::Post::Multipart.new(uri.path,
185
+ 'file' => UploadIO.convert!(
186
+ file,
187
+ 'application/octet-stream',
188
+ name,
189
+ local_path))
190
+ request['authorization'] = oauth_signature.join(', ')
191
+
192
+ response = Net::HTTP.start(uri.host, uri.port) { |http| http.request(request) }
193
+ if response.kind_of?(Net::HTTPSuccess) then
194
+ begin
195
+ return JSON.parse(response.body).symbolize_keys_recursively.to_struct_recursively
196
+ rescue JSON::ParserError
197
+ raise ParseError.new(uri.to_s, response)
198
+ end
199
+ else
200
+ raise UnsuccessfulResponseError.new(uri.to_s, response)
201
+ end
202
+ end
203
+
204
+ # Copies the +source+ file to the path at +target+. If +target+ ends with a
205
+ # slash, the new file will share the same name as the old file. Returns a
206
+ # +Struct+ with metadata for the new file. (See the metadata method.)
207
+ #
208
+ # Both paths are assumed to be relative to the configured mode's root.
209
+ #
210
+ # Raises FileNotFoundError if +source+ does not exist. Raises
211
+ # FileExistsError if +target+ already exists.
212
+ #
213
+ # Options:
214
+ #
215
+ # +mode+:: Temporarily changes the API mode. See the MODES array.
216
+ #
217
+ # TODO The API documentation says this method returns 404/403 if the source or target is invalid, but it actually returns 5xx.
218
+
219
+ def copy(source, target, options={})
220
+ source.sub! /^\//, ''
221
+ target.sub! /^\//, ''
222
+ target << File.basename(source) if target.ends_with?('/')
223
+ begin
224
+ 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
225
+ rescue UnsuccessfulResponseError => error
226
+ raise FileNotFoundError.new(source) if error.response.kind_of?(Net::HTTPNotFound)
227
+ raise FileExistsError.new(target) if error.response.kind_of?(Net::HTTPForbidden)
228
+ raise error
229
+ end
230
+ end
231
+ alias :cp :copy
232
+
233
+ # Creates a folder at the given path. The path is assumed to be relative to
234
+ # the configured mode's root. Returns a +Struct+ with metadata about the new
235
+ # folder. (See the metadata method.)
236
+ #
237
+ # Raises FileExistsError if there is already a file or folder at +path+.
238
+ #
239
+ # Options:
240
+ #
241
+ # +mode+:: Temporarily changes the API mode. See the MODES array.
242
+ #
243
+ # 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.
244
+
245
+ def create_folder(path, options={})
246
+ path.sub! /^\//, ''
247
+ path.sub! /\/$/, ''
248
+ begin
249
+ parse_metadata(post('fileops', 'create_folder', :path => Dropbox.check_path(path), :root => root(options), :ssl => @ssl)).to_struct_recursively
250
+ rescue UnsuccessfulResponseError => error
251
+ raise FileExistsError.new(path) if error.response.kind_of?(Net::HTTPForbidden)
252
+ raise error
253
+ end
254
+ end
255
+ alias :mkdir :create_folder
256
+
257
+ # Deletes a file or folder at the given path. The path is assumed to be
258
+ # relative to the configured mode's root.
259
+ #
260
+ # Raises FileNotFoundError if the file or folder does not exist at +path+.
261
+ #
262
+ # Options:
263
+ #
264
+ # +mode+:: Temporarily changes the API mode. See the MODES array.
265
+ #
266
+ # TODO The API documentation says this method returns 404 if the path does not exist, but it actually returns 5xx.
267
+
268
+ def delete(path, options={})
269
+ path.sub! /^\//, ''
270
+ path.sub! /\/$/, ''
271
+ begin
272
+ api_response(:post, 'fileops', 'delete', :path => Dropbox.check_path(path), :root => root(options), :ssl => @ssl)
273
+ rescue UnsuccessfulResponseError => error
274
+ raise FileNotFoundError.new(path) if error.response.kind_of?(Net::HTTPNotFound)
275
+ raise error
276
+ end
277
+ return true
278
+ end
279
+ alias :rm :delete
280
+
281
+ # Moves the +source+ file to the path at +target+. If +target+ ends with a
282
+ # slash, the file name will remain unchanged. If +source+ and +target+ share
283
+ # the same path but have differing file names, the file will be renamed (see
284
+ # also the rename method). Returns a +Struct+ with metadata for the new
285
+ # file. (See the metadata method.)
286
+ #
287
+ # Both paths are assumed to be relative to the configured mode's root.
288
+ #
289
+ # Raises FileNotFoundError if +source+ does not exist. Raises
290
+ # FileExistsError if +target+ already exists.
291
+ #
292
+ # Options:
293
+ #
294
+ # +mode+:: Temporarily changes the API mode. See the MODES array.
295
+ #
296
+ # TODO The API documentation says this method returns 404/403 if the source or target is invalid, but it actually returns 5xx.
297
+
298
+ def move(source, target, options={})
299
+ source.sub! /^\//, ''
300
+ target.sub! /^\//, ''
301
+ target << File.basename(source) if target.ends_with?('/')
302
+ begin
303
+ 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
304
+ rescue UnsuccessfulResponseError => error
305
+ raise FileNotFoundError.new(source) if error.response.kind_of?(Net::HTTPNotFound)
306
+ raise FileExistsError.new(target) if error.response.kind_of?(Net::HTTPForbidden)
307
+ raise error
308
+ end
309
+ end
310
+ alias :mv :move
311
+
312
+ # Renames a file. Takes the same options and raises the same exceptions as
313
+ # the move method.
314
+ #
315
+ # Calling
316
+ #
317
+ # session.rename 'path/to/file', 'new_name'
318
+ #
319
+ # is equivalent to calling
320
+ #
321
+ # session.move 'path/to/file', 'path/to/new_name'
322
+
323
+ def rename(path, new_name, options={})
324
+ raise ArgumentError, "Names cannot have slashes in them" if new_name.include?('/')
325
+ path.sub! /\/$/, ''
326
+ destination = path.split('/')
327
+ destination[destination.size - 1] = new_name
328
+ destination = destination.join('/')
329
+ move path, destination, options
330
+ end
331
+
332
+ # Returns a cookie-protected URL that the authorized user can use to view
333
+ # the file at the given path. This URL requires an authorized user.
334
+ #
335
+ # The path is assumed to be relative to the configured mode's root.
336
+ #
337
+ # Options:
338
+ #
339
+ # +mode+:: Temporarily changes the API mode. See the MODES array.
340
+
341
+ def link(path, options={})
342
+ path.sub! /^\//, ''
343
+ begin
344
+ rest = Dropbox.check_path(path).split('/')
345
+ rest << { :ssl => @ssl }
346
+ api_response(:get, 'links', root(options), *rest)
347
+ rescue UnsuccessfulResponseError => error
348
+ return error.response['Location'] if error.response.kind_of?(Net::HTTPFound)
349
+ #TODO shouldn't be using rescue blocks for normal program flow
350
+ raise error
351
+ end
352
+ end
353
+ memoize :link
354
+
355
+ # Returns a +Struct+ containing metadata on a given file or folder. The path
356
+ # is assumed to be relative to the configured mode's root.
357
+ #
358
+ # If you pass a directory for +path+, the metadata will also contain a
359
+ # listing of the directory contents (unless the +suppress_list+ option is
360
+ # true).
361
+ #
362
+ # For information on the schema of the return struct, see the Dropbox API
363
+ # at https://www.dropbox.com/developers/docs#metadata
364
+ #
365
+ # The +modified+ key will be converted into a +Time+ instance. The +is_dir+
366
+ # key will also be available as <tt>directory?</tt>.
367
+ #
368
+ # Options:
369
+ #
370
+ # +suppress_list+:: Set this to true to remove the directory list from
371
+ # the result (only applicable if +path+ is a directory).
372
+ # +limit+:: Set this value to limit the number of entries returned when
373
+ # listing a directory. If the result has more than this number of
374
+ # entries, a TooManyEntriesError will be raised.
375
+ # +mode+:: Temporarily changes the API mode. See the MODES array.
376
+ #
377
+ # TODO hash option seems to return HTTPBadRequest for now
378
+
379
+ def metadata(path, options={})
380
+ path.sub! /^\//, ''
381
+ args = [
382
+ 'metadata',
383
+ root(options)
384
+ ]
385
+ args += Dropbox.check_path(path).split('/')
386
+ args << Hash.new
387
+ args.last[:file_limit] = options[:limit] if options[:limit]
388
+ #args.last[:hash] = options[:hash] if options[:hash]
389
+ args.last[:list] = !(options[:suppress_list].to_bool)
390
+ args.last[:ssl] = @ssl
391
+
392
+ begin
393
+ parse_metadata(get(*args)).to_struct_recursively
394
+ rescue UnsuccessfulResponseError => error
395
+ raise TooManyEntriesError.new(path) if error.response.kind_of?(Net::HTTPNotAcceptable)
396
+ raise FileNotFoundError.new(path) if error.response.kind_of?(Net::HTTPNotFound)
397
+ #return :not_modified if error.kind_of?(Net::HTTPNotModified)
398
+ raise error
399
+ end
400
+ end
401
+ memoize :metadata
402
+ alias :info :metadata
403
+
404
+ # Returns an array of <tt>Struct</tt>s with information on each file within
405
+ # the given directory. Calling
406
+ #
407
+ # session.list 'my/folder'
408
+ #
409
+ # is equivalent to calling
410
+ #
411
+ # session.metadata('my/folder').contents
412
+ #
413
+ # Returns nil if the path is not a directory. Raises the same exceptions as
414
+ # the metadata method. Takes the same options as the metadata method, except
415
+ # the +suppress_list+ option is implied to be false.
416
+
417
+
418
+ def list(path, options={})
419
+ metadata(path, options.merge(:suppress_list => false)).contents
420
+ end
421
+ alias :ls :list
422
+
423
+ def event_metadata(target_events, options={}) # :nodoc:
424
+ get 'event_metadata', :ssl => @ssl, :root => root(options), :target_events => target_events
425
+ end
426
+
427
+ def event_content(entry, options={}) # :nodoc:
428
+ request = Dropbox.api_url('event_content', :target_event => entry, :ssl => @ssl, :root => root(options))
429
+ response = api_internal(:get, request)
430
+ begin
431
+ return response.body, JSON.parse(response.header['X-Dropbox-Metadata'])
432
+ rescue JSON::ParserError
433
+ raise ParseError.new(request, response)
434
+ end
435
+ end
436
+
437
+ # Returns the configured API mode.
438
+
439
+ def mode
440
+ @api_mode ||= :sandbox
441
+ end
442
+
443
+ # Sets the API mode. See the MODES array.
444
+
445
+ def mode=(newmode)
446
+ raise ArgumentError, "Unknown API mode #{newmode.inspect}" unless MODES.include?(newmode)
447
+ @api_mode = newmode
448
+ end
449
+
450
+ private
451
+
452
+ def parse_metadata(hsh)
453
+ hsh[:modified] = Time.parse(hsh[:modified]) if hsh[:modified]
454
+ hsh[:directory?] = hsh[:is_dir]
455
+ hsh.each { |_,v| parse_metadata(v) if v.kind_of?(Hash) }
456
+ hsh.each { |_,v| v.each { |h| parse_metadata(h) if h.kind_of?(Hash) } if v.kind_of?(Array) }
457
+ hsh
458
+ end
459
+
460
+ def root(options={})
461
+ api_mode = options[:mode] || mode
462
+ raise ArgumentError, "Unknown API mode #{api_mode.inspect}" unless MODES.include?(api_mode)
463
+ return api_mode == :sandbox ? 'sandbox' : 'dropbox'
464
+ end
465
+
466
+ def get(*params)
467
+ api_json :get, *params
468
+ end
469
+
470
+ def post(*params)
471
+ api_json :post, *params
472
+ end
473
+
474
+ def api_internal(method, request)
475
+ raise UnauthorizedError, "Must authorize before you can use API method" unless @access_token
476
+ response = @access_token.send(method, request)
477
+ raise UnsuccessfulResponseError.new(request, response) unless response.kind_of?(Net::HTTPSuccess)
478
+ return response
479
+ end
480
+
481
+ def api_json(method, *params)
482
+ request = Dropbox.api_url(*params)
483
+ response = api_internal(method, request)
484
+ begin
485
+ return JSON.parse(response.body).symbolize_keys_recursively
486
+ rescue JSON::ParserError
487
+ raise ParseError.new(request, response)
488
+ end
489
+ end
490
+
491
+ def api_body(method, *params)
492
+ api_response(method, *params).body
493
+ end
494
+
495
+ def api_response(method, *params)
496
+ api_internal(method, Dropbox.api_url(*params))
497
+ end
498
+ end
499
+
500
+ # Superclass for exceptions raised when the server reports an error.
501
+
502
+ class APIError < StandardError
503
+ # The request URL.
504
+ attr_reader :request
505
+ # The Net::HTTPResponse returned by the server.
506
+ attr_reader :response
507
+
508
+ def initialize(request, response) # :nodoc:
509
+ @request = request
510
+ @response = response
511
+ end
512
+
513
+ def to_s # :nodoc:
514
+ "API error: #{request}"
515
+ end
516
+ end
517
+
518
+ # Raised when the Dropbox API returns a response that was not understood.
519
+
520
+ class ParseError < APIError
521
+ def to_s # :nodoc:
522
+ "Invalid response received: #{request}"
523
+ end
524
+ end
525
+
526
+ # Raised when something other than 200 OK is returned by an API method.
527
+
528
+ class UnsuccessfulResponseError < APIError
529
+ def to_s # :nodoc:
530
+ "HTTP status #{@response.class.to_s} received: #{request}"
531
+ end
532
+ end
533
+
534
+ # Superclass of errors relating to Dropbox files.
535
+
536
+ class FileError < StandardError
537
+ # The path of the offending file.
538
+ attr_reader :path
539
+
540
+ def initialize(path) # :nodoc:
541
+ @path = path
542
+ end
543
+
544
+ def to_s # :nodoc:
545
+ "#{self.class.to_s}: #{@path}"
546
+ end
547
+ end
548
+
549
+ # Raised when a Dropbox file doesn't exist.
550
+
551
+ class FileNotFoundError < FileError; end
552
+
553
+ # Raised when a Dropbox file is in the way.
554
+
555
+ class FileExistsError < FileError; end
556
+
557
+ # Raised when the number of files within a directory exceeds a specified
558
+ # limit.
559
+
560
+ class TooManyEntriesError < FileError; end
561
+
562
+ # Raised when the event_metadata method returns an error.
563
+
564
+ class PingbackError < StandardError
565
+ # The HTTP error code returned by the event_metadata method.
566
+ attr_reader :code
567
+
568
+ def initialize(code) # :nodoc
569
+ @code = code
570
+ end
571
+
572
+ def to_s # :nodoc:
573
+ "#{self.class.to_s} code #{@code}"
574
+ end
575
+ end
576
+ end