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.
- data/.document +5 -0
- data/.gitignore +23 -3
- data/LICENSE +20 -0
- data/README.rdoc +84 -17
- data/Rakefile +39 -20
- data/VERSION +1 -1
- data/lib/dropbox.rb +41 -12
- data/lib/dropbox/api.rb +530 -0
- data/lib/dropbox/entry.rb +96 -0
- data/lib/dropbox/event.rb +109 -0
- data/lib/dropbox/memoization.rb +98 -0
- data/lib/dropbox/revision.rb +197 -0
- data/lib/dropbox/session.rb +160 -0
- data/lib/extensions/array.rb +9 -0
- data/lib/extensions/hash.rb +61 -0
- data/lib/extensions/module.rb +22 -0
- data/lib/extensions/object.rb +5 -0
- data/lib/extensions/string.rb +9 -0
- data/lib/extensions/to_bool.rb +17 -0
- data/spec/dropbox/api_spec.rb +778 -0
- data/spec/dropbox/entry_spec.rb +144 -0
- data/spec/dropbox/event_spec.rb +122 -0
- data/spec/dropbox/revision_spec.rb +367 -0
- data/spec/dropbox/session_spec.rb +148 -0
- data/spec/dropbox_spec.rb +57 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +10 -0
- metadata +64 -27
- data/ChangeLog.rdoc +0 -17
- data/examples/dropbox_spec.rb +0 -99
- data/lib/dropbox/dropbox.rb +0 -213
data/.document
ADDED
data/.gitignore
CHANGED
@@ -1,4 +1,24 @@
|
|
1
|
-
|
1
|
+
## MAC OS
|
2
2
|
.DS_Store
|
3
|
-
|
4
|
-
|
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.
|
data/README.rdoc
CHANGED
@@ -1,22 +1,89 @@
|
|
1
|
-
=
|
1
|
+
= Ruby Dropbox Gem
|
2
2
|
|
3
|
-
|
3
|
+
An easy-to-use third-party interface to the RESTful Dropbox API.
|
4
4
|
|
5
|
-
|
5
|
+
== Installation
|
6
6
|
|
7
|
-
|
8
|
-
* sudo gem install dropbox
|
7
|
+
sudo gem install rdropbox
|
9
8
|
|
10
|
-
==
|
9
|
+
== Tutorial by Example
|
11
10
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
10
|
-
|
11
|
-
|
12
|
-
gem.
|
13
|
-
gem.
|
14
|
-
gem.
|
15
|
-
|
16
|
-
gem.
|
17
|
-
gem.add_development_dependency "
|
18
|
-
gem.
|
19
|
-
gem.
|
20
|
-
|
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
|
-
|
28
|
-
Spec::Rake::SpecTask.new(
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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 :
|
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
|
1
|
+
1.0.0
|
data/lib/dropbox.rb
CHANGED
@@ -1,14 +1,43 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
require '
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
data/lib/dropbox/api.rb
ADDED
@@ -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
|