dropbox-aliix 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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