dropbox-sdk 1.6.4 → 1.6.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG +6 -0
- data/README +9 -0
- data/examples/chunked_upload.rb +47 -47
- data/examples/cli_example.rb +172 -172
- data/examples/copy_between_accounts.rb +106 -106
- data/examples/dropbox_controller.rb +70 -70
- data/examples/oauth1_upgrade.rb +28 -28
- data/examples/search_cache.rb +231 -231
- data/examples/web_file_browser.rb +111 -111
- data/lib/dropbox_sdk.rb +1273 -1228
- metadata +66 -51
@@ -6,13 +6,13 @@
|
|
6
6
|
#
|
7
7
|
# To set up:
|
8
8
|
# 1. Create a Dropbox App key and secret to use the API. https://www.dropbox.com/developers
|
9
|
-
# 2. Add http://localhost:
|
9
|
+
# 2. Add http://localhost:5000/dropbox-auth-finish as a Redirect URI for your Dropbox app.
|
10
10
|
# 3. Copy your App key and App secret into APP_KEY and APP_SECRET below.
|
11
11
|
#
|
12
12
|
# To run:
|
13
13
|
# 1. Install Sinatra $ gem install sinatra
|
14
14
|
# 2. Launch server $ ruby web_file_browser.rb
|
15
|
-
# 3. Browse to http://localhost:
|
15
|
+
# 3. Browse to http://localhost:5000/
|
16
16
|
# -------------------------------------------------------------------
|
17
17
|
|
18
18
|
require 'rubygems'
|
@@ -29,150 +29,150 @@ APP_SECRET = ''
|
|
29
29
|
# OAuth stuff
|
30
30
|
|
31
31
|
def get_web_auth()
|
32
|
-
|
33
|
-
|
32
|
+
return DropboxOAuth2Flow.new(APP_KEY, APP_SECRET, url('/dropbox-auth-finish'),
|
33
|
+
session, :dropbox_auth_csrf_token)
|
34
34
|
end
|
35
35
|
|
36
36
|
get '/dropbox-auth-start' do
|
37
|
-
|
37
|
+
authorize_url = get_web_auth().start()
|
38
38
|
|
39
|
-
|
40
|
-
|
41
|
-
|
39
|
+
# Send the user to the Dropbox website so they can authorize our app. After the user
|
40
|
+
# authorizes our app, Dropbox will redirect them to our '/dropbox-auth-finish' endpoint.
|
41
|
+
redirect authorize_url
|
42
42
|
end
|
43
43
|
|
44
44
|
get '/dropbox-auth-finish' do
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
45
|
+
begin
|
46
|
+
access_token, user_id, url_state = get_web_auth.finish(params)
|
47
|
+
rescue DropboxOAuth2Flow::BadRequestError => e
|
48
|
+
return html_page "Error in OAuth 2 flow", "<p>Bad request to /dropbox-auth-finish: #{e}</p>"
|
49
|
+
rescue DropboxOAuth2Flow::BadStateError => e
|
50
|
+
return html_page "Error in OAuth 2 flow", "<p>Auth session expired: #{e}</p>"
|
51
|
+
rescue DropboxOAuth2Flow::CsrfError => e
|
52
|
+
logger.info("/dropbox-auth-finish: CSRF mismatch: #{e}")
|
53
|
+
return html_page "Error in OAuth 2 flow", "<p>CSRF mismatch</p>"
|
54
|
+
rescue DropboxOAuth2Flow::NotApprovedError => e
|
55
|
+
return html_page "Not Approved?", "<p>Why not, bro?</p>"
|
56
|
+
rescue DropboxOAuth2Flow::ProviderError => e
|
57
|
+
return html_page "Error in OAuth 2 flow", "Error redirect from Dropbox: #{e}"
|
58
|
+
rescue DropboxError => e
|
59
|
+
logger.info "Error getting OAuth 2 access token: #{e}"
|
60
|
+
return html_page "Error in OAuth 2 flow", "<p>Error getting access token</p>"
|
61
|
+
end
|
62
|
+
|
63
|
+
# In this simple example, we store the authorized DropboxSession in the session.
|
64
|
+
# A real webapp might store it somewhere more persistent.
|
65
|
+
session[:access_token] = access_token
|
66
|
+
redirect url('/')
|
67
67
|
end
|
68
68
|
|
69
69
|
get '/dropbox-unlink' do
|
70
|
-
|
71
|
-
|
70
|
+
session.delete(:access_token)
|
71
|
+
nil
|
72
72
|
end
|
73
73
|
|
74
74
|
# If we already have an authorized DropboxSession, returns a DropboxClient.
|
75
75
|
def get_dropbox_client
|
76
|
-
|
77
|
-
|
78
|
-
|
76
|
+
if session[:access_token]
|
77
|
+
return DropboxClient.new(session[:access_token])
|
78
|
+
end
|
79
79
|
end
|
80
80
|
|
81
81
|
# -------------------------------------------------------------------
|
82
82
|
# File/folder display stuff
|
83
83
|
|
84
84
|
get '/' do
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
else
|
103
|
-
logger.info "Dropbox API error: #{e}"
|
104
|
-
return html_page "Dropbox API error"
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
|
-
if entry['is_dir']
|
109
|
-
render_folder(client, entry)
|
85
|
+
# Get the DropboxClient object. Redirect to OAuth flow if necessary.
|
86
|
+
client = get_dropbox_client
|
87
|
+
unless client
|
88
|
+
redirect url("/dropbox-auth-start")
|
89
|
+
end
|
90
|
+
|
91
|
+
# Call DropboxClient.metadata
|
92
|
+
path = params[:path] || '/'
|
93
|
+
begin
|
94
|
+
entry = client.metadata(path)
|
95
|
+
rescue DropboxAuthError => e
|
96
|
+
session.delete(:access_token) # An auth error means the access token is probably bad
|
97
|
+
logger.info "Dropbox auth error: #{e}"
|
98
|
+
return html_page "Dropbox auth error"
|
99
|
+
rescue DropboxError => e
|
100
|
+
if e.http_response.code == '404'
|
101
|
+
return html_page "Path not found: #{h path}"
|
110
102
|
else
|
111
|
-
|
103
|
+
logger.info "Dropbox API error: #{e}"
|
104
|
+
return html_page "Dropbox API error"
|
112
105
|
end
|
106
|
+
end
|
107
|
+
|
108
|
+
if entry['is_dir']
|
109
|
+
render_folder(client, entry)
|
110
|
+
else
|
111
|
+
render_file(client, entry)
|
112
|
+
end
|
113
113
|
end
|
114
114
|
|
115
115
|
def render_folder(client, entry)
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
116
|
+
# Provide an upload form (so the user can add files to this folder)
|
117
|
+
out = "<form action='/upload' method='post' enctype='multipart/form-data'>"
|
118
|
+
out += "<label for='file'>Upload file:</label> <input name='file' type='file'/>"
|
119
|
+
out += "<input type='submit' value='Upload'/>"
|
120
|
+
out += "<input name='folder' type='hidden' value='#{h entry['path']}'/>"
|
121
|
+
out += "</form>" # TODO: Add a token to counter CSRF attacks.
|
122
|
+
# List of folder contents
|
123
|
+
entry['contents'].each do |child|
|
124
|
+
cp = child['path'] # child path
|
125
|
+
cn = File.basename(cp) # child name
|
126
|
+
if (child['is_dir']) then cn += '/' end
|
127
|
+
out += "<div><a style='text-decoration: none' href='/?path=#{h cp}'>#{h cn}</a></div>"
|
128
|
+
end
|
129
|
+
|
130
|
+
html_page "Folder: #{entry['path']}", out
|
131
131
|
end
|
132
132
|
|
133
133
|
def render_file(client, entry)
|
134
|
-
|
135
|
-
|
134
|
+
# Just dump out metadata hash
|
135
|
+
html_page "File: #{entry['path']}", "<pre>#{h entry.pretty_inspect}</pre>"
|
136
136
|
end
|
137
137
|
|
138
138
|
# -------------------------------------------------------------------
|
139
139
|
# File upload handler
|
140
140
|
|
141
141
|
post '/upload' do
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
142
|
+
# Check POST parameter.
|
143
|
+
file = params[:file]
|
144
|
+
unless file && (temp_file = file[:tempfile]) && (name = file[:filename])
|
145
|
+
return html_page "Upload error", "<p>No file selected.</p>"
|
146
|
+
end
|
147
|
+
|
148
|
+
# Get the DropboxClient object.
|
149
|
+
client = get_dropbox_client
|
150
|
+
unless client
|
151
|
+
return html_page "Upload error", "<p>Not linked with a Dropbox account.</p>"
|
152
|
+
end
|
153
|
+
|
154
|
+
# Call DropboxClient.put_file
|
155
|
+
begin
|
156
|
+
entry = client.put_file("#{params[:folder]}/#{name}", temp_file.read)
|
157
|
+
rescue DropboxAuthError => e
|
158
|
+
session.delete(:access_token) # An auth error means the access token is probably bad
|
159
|
+
logger.info "Dropbox auth error: #{e}"
|
160
|
+
return html_page "Dropbox auth error"
|
161
|
+
rescue DropboxError => e
|
162
|
+
logger.info "Dropbox API error: #{e}"
|
163
|
+
return html_page "Dropbox API error"
|
164
|
+
end
|
165
|
+
|
166
|
+
html_page "Upload complete", "<pre>#{h entry.pretty_inspect}</pre>"
|
167
167
|
end
|
168
168
|
|
169
169
|
# -------------------------------------------------------------------
|
170
170
|
|
171
171
|
def html_page(title, body='')
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
172
|
+
"<html>" +
|
173
|
+
"<head><title>#{h title}</title></head>" +
|
174
|
+
"<body><h1>#{h title}</h1>#{body}</body>" +
|
175
|
+
"</html>"
|
176
176
|
end
|
177
177
|
|
178
178
|
# Rack will issue a warning if no session secret key is set. A real web app would not have
|
@@ -183,11 +183,11 @@ set :port, 5000
|
|
183
183
|
enable :sessions
|
184
184
|
|
185
185
|
helpers do
|
186
|
-
|
187
|
-
|
186
|
+
include Rack::Utils
|
187
|
+
alias_method :h, :escape_html
|
188
188
|
end
|
189
189
|
|
190
190
|
if APP_KEY == '' or APP_SECRET == ''
|
191
|
-
|
192
|
-
|
191
|
+
puts "You must set APP_KEY and APP_SECRET at the top of \"#{__FILE__}\"!"
|
192
|
+
exit 1
|
193
193
|
end
|
data/lib/dropbox_sdk.rb
CHANGED
@@ -8,208 +8,222 @@ require 'securerandom'
|
|
8
8
|
require 'pp'
|
9
9
|
|
10
10
|
module Dropbox # :nodoc:
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
end
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
11
|
+
API_SERVER = "api.dropbox.com"
|
12
|
+
API_CONTENT_SERVER = "api-content.dropbox.com"
|
13
|
+
API_NOTIFY_SERVER = "api-notify.dropbox.com"
|
14
|
+
WEB_SERVER = "www.dropbox.com"
|
15
|
+
|
16
|
+
SERVERS = {
|
17
|
+
:api => API_SERVER,
|
18
|
+
:content => API_CONTENT_SERVER,
|
19
|
+
:notify => API_NOTIFY_SERVER,
|
20
|
+
:web => WEB_SERVER
|
21
|
+
}
|
22
|
+
|
23
|
+
API_VERSION = 1
|
24
|
+
SDK_VERSION = "1.6.5"
|
25
|
+
|
26
|
+
TRUSTED_CERT_FILE = File.join(File.dirname(__FILE__), 'trusted-certs.crt')
|
27
|
+
|
28
|
+
def self.clean_params(params)
|
29
|
+
r = {}
|
30
|
+
params.each do |k, v|
|
31
|
+
r[k] = v.to_s if not v.nil?
|
32
|
+
end
|
33
|
+
r
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.make_query_string(params)
|
37
|
+
clean_params(params).collect {|k, v|
|
38
|
+
CGI.escape(k) + "=" + CGI.escape(v)
|
39
|
+
}.join("&")
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.do_http(uri, request) # :nodoc:
|
43
|
+
|
44
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
45
|
+
|
46
|
+
http.use_ssl = true
|
47
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
48
|
+
http.ca_file = Dropbox::TRUSTED_CERT_FILE
|
49
|
+
http.read_timeout = 600
|
50
|
+
|
51
|
+
if RUBY_VERSION >= '1.9'
|
52
|
+
# SSL protocol and ciphersuite settings are supported strating with version 1.9
|
53
|
+
http.ssl_version = 'TLSv1'
|
54
|
+
http.ciphers = 'ECDHE-RSA-AES256-GCM-SHA384:'\
|
55
|
+
'ECDHE-RSA-AES256-SHA384:'\
|
56
|
+
'ECDHE-RSA-AES256-SHA:'\
|
57
|
+
'ECDHE-RSA-AES128-GCM-SHA256:'\
|
58
|
+
'ECDHE-RSA-AES128-SHA256:'\
|
59
|
+
'ECDHE-RSA-AES128-SHA:'\
|
60
|
+
'ECDHE-RSA-RC4-SHA:'\
|
61
|
+
'DHE-RSA-AES256-GCM-SHA384:'\
|
62
|
+
'DHE-RSA-AES256-SHA256:'\
|
63
|
+
'DHE-RSA-AES256-SHA:'\
|
64
|
+
'DHE-RSA-AES128-GCM-SHA256:'\
|
65
|
+
'DHE-RSA-AES128-SHA256:'\
|
66
|
+
'DHE-RSA-AES128-SHA:'\
|
67
|
+
'AES256-GCM-SHA384:'\
|
68
|
+
'AES256-SHA256:'\
|
69
|
+
'AES256-SHA:'\
|
70
|
+
'AES128-GCM-SHA256:'\
|
71
|
+
'AES128-SHA256:'\
|
72
|
+
'AES128-SHA'
|
73
|
+
end
|
74
|
+
|
75
|
+
# Important security note!
|
76
|
+
# Some Ruby versions (e.g. the one that ships with OS X) do not raise
|
77
|
+
# an exception if certificate validation fails. We therefore have to
|
78
|
+
# add a custom callback to ensure that invalid certs are not accepted.
|
79
|
+
# Some specific error codes are let through, so we change the error
|
80
|
+
# code to make sure that Ruby throws an exception if certificate
|
81
|
+
# validation fails.
|
82
|
+
#
|
83
|
+
# See the man page for 'verify' for more information on error codes.
|
84
|
+
#
|
85
|
+
# You can comment out this code if your Ruby version is not vulnerable.
|
86
|
+
http.verify_callback = proc do |preverify_ok, ssl_context|
|
87
|
+
# 0 is the error code for success
|
88
|
+
if preverify_ok && ssl_context.error == 0
|
89
|
+
true
|
90
|
+
else
|
91
|
+
# 7 is the error code for certification signature failure
|
92
|
+
ssl_context.error = 7
|
93
|
+
false
|
38
94
|
end
|
39
|
-
true
|
40
95
|
end
|
41
96
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
# Important security note!
|
75
|
-
# Some Ruby versions (e.g. the one that ships with OS X) do not raise an exception if certificate validation fails.
|
76
|
-
# We therefore have to add a custom callback to ensure that invalid certs are not accepted
|
77
|
-
# See https://www.braintreepayments.com/braintrust/sslsocket-verify_mode-doesnt-verify
|
78
|
-
# You can comment out this code in case your Ruby version is not vulnerable
|
79
|
-
http.verify_callback = proc do |preverify_ok, ssl_context|
|
80
|
-
Dropbox::verify_ssl_certificate(preverify_ok, ssl_context)
|
81
|
-
end
|
82
|
-
|
83
|
-
#We use this to better understand how developers are using our SDKs.
|
84
|
-
request['User-Agent'] = "OfficialDropboxRubySDK/#{Dropbox::SDK_VERSION}"
|
85
|
-
|
86
|
-
begin
|
87
|
-
http.request(request)
|
88
|
-
rescue OpenSSL::SSL::SSLError => e
|
89
|
-
raise DropboxError.new("SSL error connecting to Dropbox. " +
|
90
|
-
"There may be a problem with the set of certificates in \"#{Dropbox::TRUSTED_CERT_FILE}\". #{e.message}")
|
91
|
-
end
|
97
|
+
#We use this to better understand how developers are using our SDKs.
|
98
|
+
request['User-Agent'] = "OfficialDropboxRubySDK/#{Dropbox::SDK_VERSION}"
|
99
|
+
|
100
|
+
begin
|
101
|
+
http.request(request)
|
102
|
+
rescue OpenSSL::SSL::SSLError => e
|
103
|
+
raise DropboxError.new("SSL error connecting to Dropbox. " +
|
104
|
+
"There may be a problem with the set of certificates in \"#{Dropbox::TRUSTED_CERT_FILE}\". #{e.message}")
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Parse response. You probably shouldn't be calling this directly. This takes responses from the server
|
109
|
+
# and parses them. It also checks for errors and raises exceptions with the appropriate messages.
|
110
|
+
def self.parse_response(response, raw=false) # :nodoc:
|
111
|
+
if response.is_a?(Net::HTTPServerError)
|
112
|
+
raise DropboxError.new("Dropbox Server Error: #{response} - #{response.body}", response)
|
113
|
+
elsif response.is_a?(Net::HTTPUnauthorized)
|
114
|
+
raise DropboxAuthError.new("User is not authenticated.", response)
|
115
|
+
elsif !response.is_a?(Net::HTTPSuccess)
|
116
|
+
begin
|
117
|
+
d = JSON.parse(response.body)
|
118
|
+
rescue
|
119
|
+
raise DropboxError.new("Dropbox Server Error: body=#{response.body}", response)
|
120
|
+
end
|
121
|
+
if d['user_error'] and d['error']
|
122
|
+
raise DropboxError.new(d['error'], response, d['user_error']) #user_error is translated
|
123
|
+
elsif d['error']
|
124
|
+
raise DropboxError.new(d['error'], response)
|
125
|
+
else
|
126
|
+
raise DropboxError.new(response.body, response)
|
127
|
+
end
|
92
128
|
end
|
93
129
|
|
94
|
-
|
95
|
-
# and parses them. It also checks for errors and raises exceptions with the appropriate messages.
|
96
|
-
def self.parse_response(response, raw=false) # :nodoc:
|
97
|
-
if response.kind_of?(Net::HTTPServerError)
|
98
|
-
raise DropboxError.new("Dropbox Server Error: #{response} - #{response.body}", response)
|
99
|
-
elsif response.kind_of?(Net::HTTPUnauthorized)
|
100
|
-
raise DropboxAuthError.new("User is not authenticated.", response)
|
101
|
-
elsif not response.kind_of?(Net::HTTPSuccess)
|
102
|
-
begin
|
103
|
-
d = JSON.parse(response.body)
|
104
|
-
rescue
|
105
|
-
raise DropboxError.new("Dropbox Server Error: body=#{response.body}", response)
|
106
|
-
end
|
107
|
-
if d['user_error'] and d['error']
|
108
|
-
raise DropboxError.new(d['error'], response, d['user_error']) #user_error is translated
|
109
|
-
elsif d['error']
|
110
|
-
raise DropboxError.new(d['error'], response)
|
111
|
-
else
|
112
|
-
raise DropboxError.new(response.body, response)
|
113
|
-
end
|
114
|
-
end
|
130
|
+
return response.body if raw
|
115
131
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
rescue JSON::ParserError
|
121
|
-
raise DropboxError.new("Unable to parse JSON response: #{response.body}", response)
|
122
|
-
end
|
132
|
+
begin
|
133
|
+
return JSON.parse(response.body)
|
134
|
+
rescue JSON::ParserError
|
135
|
+
raise DropboxError.new("Unable to parse JSON response: #{response.body}", response)
|
123
136
|
end
|
137
|
+
end
|
124
138
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
end
|
139
|
+
# A string comparison function that is resistant to timing attacks. If you're comparing a
|
140
|
+
# string you got from the outside world with a string that is supposed to be a secret, use
|
141
|
+
# this function to check equality.
|
142
|
+
def self.safe_string_equals(a, b)
|
143
|
+
if a.length != b.length
|
144
|
+
false
|
145
|
+
else
|
146
|
+
a.chars.zip(b.chars).map {|ac,bc| ac == bc}.all?
|
134
147
|
end
|
148
|
+
end
|
135
149
|
end
|
136
150
|
|
137
151
|
class DropboxSessionBase # :nodoc:
|
138
152
|
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
end
|
189
|
-
request.body_stream = body
|
190
|
-
else
|
191
|
-
s = body.to_s
|
192
|
-
request["Content-Length"] = s.length
|
193
|
-
request.body = s
|
194
|
-
end
|
153
|
+
attr_writer :locale
|
154
|
+
|
155
|
+
def initialize(locale)
|
156
|
+
@locale = locale
|
157
|
+
end
|
158
|
+
|
159
|
+
private
|
160
|
+
|
161
|
+
def build_url(path, server)
|
162
|
+
port = 443
|
163
|
+
host = Dropbox::SERVERS[server]
|
164
|
+
full_path = "/#{Dropbox::API_VERSION}#{path}"
|
165
|
+
return URI::HTTPS.build({:host => host, :path => full_path})
|
166
|
+
end
|
167
|
+
|
168
|
+
def build_url_with_params(path, params, server) # :nodoc:
|
169
|
+
target = build_url(path, server)
|
170
|
+
params['locale'] = @locale
|
171
|
+
target.query = Dropbox::make_query_string(params)
|
172
|
+
return target
|
173
|
+
end
|
174
|
+
|
175
|
+
protected
|
176
|
+
|
177
|
+
def do_http(uri, request) # :nodoc:
|
178
|
+
sign_request(request)
|
179
|
+
Dropbox::do_http(uri, request)
|
180
|
+
end
|
181
|
+
|
182
|
+
public
|
183
|
+
|
184
|
+
def do_get(path, params=nil, server=:api) # :nodoc:
|
185
|
+
params ||= {}
|
186
|
+
assert_authorized
|
187
|
+
uri = build_url_with_params(path, params, server)
|
188
|
+
do_http(uri, Net::HTTP::Get.new(uri.request_uri))
|
189
|
+
end
|
190
|
+
|
191
|
+
def do_http_with_body(uri, request, body)
|
192
|
+
if body != nil
|
193
|
+
if body.is_a?(Hash)
|
194
|
+
request.set_form_data(Dropbox::clean_params(body))
|
195
|
+
elsif body.respond_to?(:read)
|
196
|
+
if body.respond_to?(:length)
|
197
|
+
request["Content-Length"] = body.length.to_s
|
198
|
+
elsif body.respond_to?(:stat) && body.stat.respond_to?(:size)
|
199
|
+
request["Content-Length"] = body.stat.size.to_s
|
200
|
+
else
|
201
|
+
raise ArgumentError, "Don't know how to handle 'body' (responds to 'read' but not to 'length' or 'stat.size')."
|
195
202
|
end
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
uri = build_url(path, content_server)
|
203
|
-
params['locale'] = @locale
|
204
|
-
do_http_with_body(uri, Net::HTTP::Post.new(uri.request_uri, headers), params)
|
205
|
-
end
|
206
|
-
|
207
|
-
def do_put(path, params=nil, headers=nil, body=nil, content_server=false) # :nodoc:
|
208
|
-
params ||= {}
|
209
|
-
assert_authorized
|
210
|
-
uri = build_url_with_params(path, params, content_server)
|
211
|
-
do_http_with_body(uri, Net::HTTP::Put.new(uri.request_uri, headers), body)
|
203
|
+
request.body_stream = body
|
204
|
+
else
|
205
|
+
s = body.to_s
|
206
|
+
request["Content-Length"] = s.length
|
207
|
+
request.body = s
|
208
|
+
end
|
212
209
|
end
|
210
|
+
do_http(uri, request)
|
211
|
+
end
|
212
|
+
|
213
|
+
def do_post(path, params=nil, headers=nil, server=:api) # :nodoc:
|
214
|
+
params ||= {}
|
215
|
+
assert_authorized
|
216
|
+
uri = build_url(path, server)
|
217
|
+
params['locale'] = @locale
|
218
|
+
do_http_with_body(uri, Net::HTTP::Post.new(uri.request_uri, headers), params)
|
219
|
+
end
|
220
|
+
|
221
|
+
def do_put(path, params=nil, headers=nil, body=nil, server=:api) # :nodoc:
|
222
|
+
params ||= {}
|
223
|
+
assert_authorized
|
224
|
+
uri = build_url_with_params(path, params, server)
|
225
|
+
do_http_with_body(uri, Net::HTTP::Put.new(uri.request_uri, headers), body)
|
226
|
+
end
|
213
227
|
end
|
214
228
|
|
215
229
|
# DropboxSession is responsible for holding OAuth 1 information. It knows how to take your consumer key and secret
|
@@ -217,486 +231,479 @@ end
|
|
217
231
|
# DropboxClient after its been authorized.
|
218
232
|
class DropboxSession < DropboxSessionBase # :nodoc:
|
219
233
|
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
234
|
+
# * consumer_key - Your Dropbox application's "app key".
|
235
|
+
# * consumer_secret - Your Dropbox application's "app secret".
|
236
|
+
def initialize(consumer_key, consumer_secret, locale=nil)
|
237
|
+
super(locale)
|
238
|
+
@consumer_key = consumer_key
|
239
|
+
@consumer_secret = consumer_secret
|
240
|
+
@request_token = nil
|
241
|
+
@access_token = nil
|
242
|
+
end
|
243
|
+
|
244
|
+
private
|
245
|
+
|
246
|
+
def build_auth_header(token) # :nodoc:
|
247
|
+
header = "OAuth oauth_version=\"1.0\", oauth_signature_method=\"PLAINTEXT\", " +
|
248
|
+
"oauth_consumer_key=\"#{URI.escape(@consumer_key)}\", "
|
249
|
+
if token
|
250
|
+
key = URI.escape(token.key)
|
251
|
+
secret = URI.escape(token.secret)
|
252
|
+
header += "oauth_token=\"#{key}\", oauth_signature=\"#{URI.escape(@consumer_secret)}&#{secret}\""
|
253
|
+
else
|
254
|
+
header += "oauth_signature=\"#{URI.escape(@consumer_secret)}&\""
|
228
255
|
end
|
256
|
+
header
|
257
|
+
end
|
229
258
|
|
230
|
-
|
259
|
+
def do_get_with_token(url, token) # :nodoc:
|
260
|
+
uri = URI.parse(url)
|
261
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
262
|
+
request.add_field('Authorization', build_auth_header(token))
|
263
|
+
Dropbox::do_http(uri, request)
|
264
|
+
end
|
231
265
|
|
232
|
-
|
233
|
-
header = "OAuth oauth_version=\"1.0\", oauth_signature_method=\"PLAINTEXT\", " +
|
234
|
-
"oauth_consumer_key=\"#{URI.escape(@consumer_key)}\", "
|
235
|
-
if token
|
236
|
-
key = URI.escape(token.key)
|
237
|
-
secret = URI.escape(token.secret)
|
238
|
-
header += "oauth_token=\"#{key}\", oauth_signature=\"#{URI.escape(@consumer_secret)}&#{secret}\""
|
239
|
-
else
|
240
|
-
header += "oauth_signature=\"#{URI.escape(@consumer_secret)}&\""
|
241
|
-
end
|
242
|
-
header
|
243
|
-
end
|
266
|
+
protected
|
244
267
|
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
268
|
+
def sign_request(request) # :nodoc:
|
269
|
+
request.add_field('Authorization', build_auth_header(@access_token))
|
270
|
+
end
|
271
|
+
|
272
|
+
public
|
273
|
+
|
274
|
+
def get_token(url_end, input_token, error_message_prefix) #: nodoc:
|
275
|
+
response = do_get_with_token("https://#{Dropbox::API_SERVER}:443/#{Dropbox::API_VERSION}/oauth#{url_end}", input_token)
|
276
|
+
if not response.kind_of?(Net::HTTPSuccess) # it must be a 200
|
277
|
+
raise DropboxAuthError.new("#{error_message_prefix} Server returned #{response.code}: #{response.message}.", response)
|
250
278
|
end
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
279
|
+
parts = CGI.parse(response.body)
|
280
|
+
|
281
|
+
if !parts.has_key? "oauth_token" and parts["oauth_token"].length != 1
|
282
|
+
raise DropboxAuthError.new("Invalid response from #{url_end}: missing \"oauth_token\" parameter: #{response.body}", response)
|
283
|
+
end
|
284
|
+
if !parts.has_key? "oauth_token_secret" and parts["oauth_token_secret"].length != 1
|
285
|
+
raise DropboxAuthError.new("Invalid response from #{url_end}: missing \"oauth_token\" parameter: #{response.body}", response)
|
256
286
|
end
|
257
287
|
|
258
|
-
|
288
|
+
OAuthToken.new(parts["oauth_token"][0], parts["oauth_token_secret"][0])
|
289
|
+
end
|
259
290
|
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
end
|
265
|
-
parts = CGI.parse(response.body)
|
291
|
+
# This returns a request token. Requests one from the dropbox server using the provided application key and secret if nessecary.
|
292
|
+
def get_request_token()
|
293
|
+
@request_token ||= get_token("/request_token", nil, "Error getting request token. Is your app key and secret correctly set?")
|
294
|
+
end
|
266
295
|
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
raise DropboxAuthError.new("Invalid response from #{url_end}: missing \"oauth_token\" parameter: #{response.body}", response)
|
272
|
-
end
|
296
|
+
# This returns a URL that your user must visit to grant
|
297
|
+
# permissions to this application.
|
298
|
+
def get_authorize_url(callback=nil)
|
299
|
+
get_request_token()
|
273
300
|
|
274
|
-
|
301
|
+
url = "/#{Dropbox::API_VERSION}/oauth/authorize?oauth_token=#{URI.escape(@request_token.key)}"
|
302
|
+
if callback
|
303
|
+
url += "&oauth_callback=#{URI.escape(callback)}"
|
275
304
|
end
|
276
|
-
|
277
|
-
|
278
|
-
def get_request_token()
|
279
|
-
@request_token ||= get_token("/request_token", nil, "Error getting request token. Is your app key and secret correctly set?")
|
305
|
+
if @locale
|
306
|
+
url += "&locale=#{URI.escape(@locale)}"
|
280
307
|
end
|
281
308
|
|
282
|
-
#
|
283
|
-
|
284
|
-
def get_authorize_url(callback=nil)
|
285
|
-
get_request_token()
|
309
|
+
"https://#{Dropbox::WEB_SERVER}#{url}"
|
310
|
+
end
|
286
311
|
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
if @locale
|
292
|
-
url += "&locale=#{URI.escape(@locale)}"
|
293
|
-
end
|
312
|
+
# Clears the access_token
|
313
|
+
def clear_access_token
|
314
|
+
@access_token = nil
|
315
|
+
end
|
294
316
|
|
295
|
-
|
296
|
-
|
317
|
+
# Returns the request token, or nil if one hasn't been acquired yet.
|
318
|
+
def request_token
|
319
|
+
@request_token
|
320
|
+
end
|
297
321
|
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
322
|
+
# Returns the access token, or nil if one hasn't been acquired yet.
|
323
|
+
def access_token
|
324
|
+
@access_token
|
325
|
+
end
|
302
326
|
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
327
|
+
# Given a saved request token and secret, set this location's token and secret
|
328
|
+
# * token - this is the request token
|
329
|
+
# * secret - this is the request token secret
|
330
|
+
def set_request_token(key, secret)
|
331
|
+
@request_token = OAuthToken.new(key, secret)
|
332
|
+
end
|
307
333
|
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
334
|
+
# Given a saved access token and secret, you set this Session to use that token and secret
|
335
|
+
# * token - this is the access token
|
336
|
+
# * secret - this is the access token secret
|
337
|
+
def set_access_token(key, secret)
|
338
|
+
@access_token = OAuthToken.new(key, secret)
|
339
|
+
end
|
312
340
|
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
end
|
341
|
+
# Returns the access token. If this DropboxSession doesn't yet have an access_token, it requests one
|
342
|
+
# using the request_token generate from your app's token and secret. This request will fail unless
|
343
|
+
# your user has gone to the authorize_url and approved your request
|
344
|
+
def get_access_token
|
345
|
+
return @access_token if authorized?
|
319
346
|
|
320
|
-
|
321
|
-
|
322
|
-
# * secret - this is the access token secret
|
323
|
-
def set_access_token(key, secret)
|
324
|
-
@access_token = OAuthToken.new(key, secret)
|
347
|
+
if @request_token.nil?
|
348
|
+
raise RuntimeError.new("No request token. You must set this or get an authorize url first.")
|
325
349
|
end
|
326
350
|
|
327
|
-
|
328
|
-
|
329
|
-
# your user has gone to the authorize_url and approved your request
|
330
|
-
def get_access_token
|
331
|
-
return @access_token if authorized?
|
351
|
+
@access_token = get_token("/access_token", @request_token, "Couldn't get access token.")
|
352
|
+
end
|
332
353
|
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
@access_token = get_token("/access_token", @request_token, "Couldn't get access token.")
|
354
|
+
# If we have an access token, then do nothing. If not, throw a RuntimeError.
|
355
|
+
def assert_authorized
|
356
|
+
unless authorized?
|
357
|
+
raise RuntimeError.new('Session does not yet have a request token')
|
338
358
|
end
|
359
|
+
end
|
339
360
|
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
end
|
345
|
-
end
|
361
|
+
# Returns true if this Session has been authorized and has an access_token.
|
362
|
+
def authorized?
|
363
|
+
!!@access_token
|
364
|
+
end
|
346
365
|
|
347
|
-
|
348
|
-
|
349
|
-
|
366
|
+
# serialize the DropboxSession.
|
367
|
+
# At DropboxSession's state is capture in three key/secret pairs. Consumer, request, and access.
|
368
|
+
# Serialize returns these in a YAML string, generated from a converted array of the form:
|
369
|
+
# [consumer_key, consumer_secret, request_token.token, request_token.secret, access_token.token, access_token.secret]
|
370
|
+
# access_token is only included if it already exists in the DropboxSesssion
|
371
|
+
def serialize
|
372
|
+
toreturn = []
|
373
|
+
if @access_token
|
374
|
+
toreturn.push @access_token.secret, @access_token.key
|
350
375
|
end
|
351
376
|
|
352
|
-
|
353
|
-
# At DropboxSession's state is capture in three key/secret pairs. Consumer, request, and access.
|
354
|
-
# Serialize returns these in a YAML string, generated from a converted array of the form:
|
355
|
-
# [consumer_key, consumer_secret, request_token.token, request_token.secret, access_token.token, access_token.secret]
|
356
|
-
# access_token is only included if it already exists in the DropboxSesssion
|
357
|
-
def serialize
|
358
|
-
toreturn = []
|
359
|
-
if @access_token
|
360
|
-
toreturn.push @access_token.secret, @access_token.key
|
361
|
-
end
|
377
|
+
get_request_token
|
362
378
|
|
363
|
-
|
379
|
+
toreturn.push @request_token.secret, @request_token.key
|
380
|
+
toreturn.push @consumer_secret, @consumer_key
|
364
381
|
|
365
|
-
|
366
|
-
|
382
|
+
toreturn.to_yaml
|
383
|
+
end
|
367
384
|
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
ser = YAML::load(ser)
|
374
|
-
session = DropboxSession.new(ser.pop, ser.pop)
|
375
|
-
session.set_request_token(ser.pop, ser.pop)
|
385
|
+
# Takes a serialized DropboxSession YAML String and returns a new DropboxSession object
|
386
|
+
def self.deserialize(ser)
|
387
|
+
ser = YAML::load(ser)
|
388
|
+
session = DropboxSession.new(ser.pop, ser.pop)
|
389
|
+
session.set_request_token(ser.pop, ser.pop)
|
376
390
|
|
377
|
-
|
378
|
-
|
379
|
-
end
|
380
|
-
session
|
391
|
+
if ser.length > 0
|
392
|
+
session.set_access_token(ser.pop, ser.pop)
|
381
393
|
end
|
394
|
+
session
|
395
|
+
end
|
382
396
|
end
|
383
397
|
|
384
398
|
|
385
399
|
class DropboxOAuth2Session < DropboxSessionBase # :nodoc:
|
386
400
|
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
end
|
392
|
-
@access_token = oauth2_access_token
|
401
|
+
def initialize(oauth2_access_token, locale=nil)
|
402
|
+
super(locale)
|
403
|
+
if not oauth2_access_token.is_a?(String)
|
404
|
+
raise "bad type for oauth2_access_token (expecting String)"
|
393
405
|
end
|
406
|
+
@access_token = oauth2_access_token
|
407
|
+
end
|
394
408
|
|
395
|
-
|
396
|
-
|
397
|
-
|
409
|
+
def assert_authorized
|
410
|
+
true
|
411
|
+
end
|
398
412
|
|
399
|
-
|
413
|
+
protected
|
400
414
|
|
401
|
-
|
402
|
-
|
403
|
-
|
415
|
+
def sign_request(request) # :nodoc:
|
416
|
+
request.add_field('Authorization', 'Bearer ' + @access_token)
|
417
|
+
end
|
404
418
|
end
|
405
419
|
|
406
420
|
# Base class for the two OAuth 2 authorization helpers.
|
407
421
|
class DropboxOAuth2FlowBase # :nodoc:
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
422
|
+
def initialize(consumer_key, consumer_secret, locale=nil)
|
423
|
+
if not consumer_key.is_a?(String)
|
424
|
+
raise ArgumentError, "consumer_key must be a String, got #{consumer_key.inspect}"
|
425
|
+
end
|
426
|
+
if not consumer_secret.is_a?(String)
|
427
|
+
raise ArgumentError, "consumer_secret must be a String, got #{consumer_secret.inspect}"
|
428
|
+
end
|
429
|
+
if not (locale.nil? or locale.is_a?(String))
|
430
|
+
raise ArgumentError, "locale must be a String or nil, got #{locale.inspect}"
|
431
|
+
end
|
432
|
+
@consumer_key = consumer_key
|
433
|
+
@consumer_secret = consumer_secret
|
434
|
+
@locale = locale
|
435
|
+
end
|
436
|
+
|
437
|
+
def _get_authorize_url(redirect_uri, state)
|
438
|
+
params = {
|
439
|
+
"client_id" => @consumer_key,
|
440
|
+
"response_type" => "code",
|
441
|
+
"redirect_uri" => redirect_uri,
|
442
|
+
"state" => state,
|
443
|
+
"locale" => @locale,
|
444
|
+
}
|
445
|
+
|
446
|
+
host = Dropbox::WEB_SERVER
|
447
|
+
path = "/#{Dropbox::API_VERSION}/oauth2/authorize"
|
448
|
+
|
449
|
+
target = URI::Generic.new("https", nil, host, nil, nil, path, nil, nil, nil)
|
450
|
+
target.query = Dropbox::make_query_string(params)
|
451
|
+
|
452
|
+
target.to_s
|
453
|
+
end
|
454
|
+
|
455
|
+
# Finish the OAuth 2 authorization process. If you used a redirect_uri, pass that in.
|
456
|
+
# Will return an access token string that you can use with DropboxClient.
|
457
|
+
def _finish(code, original_redirect_uri)
|
458
|
+
if not code.is_a?(String)
|
459
|
+
raise ArgumentError, "code must be a String"
|
460
|
+
end
|
461
|
+
|
462
|
+
uri = URI.parse("https://#{Dropbox::API_SERVER}/1/oauth2/token")
|
463
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
464
|
+
client_credentials = @consumer_key + ':' + @consumer_secret
|
465
|
+
request.add_field('Authorization', 'Basic ' + Base64.encode64(client_credentials).chomp("\n"))
|
466
|
+
|
467
|
+
params = {
|
468
|
+
"grant_type" => "authorization_code",
|
469
|
+
"code" => code,
|
470
|
+
"redirect_uri" => original_redirect_uri,
|
471
|
+
"locale" => @locale,
|
472
|
+
}
|
473
|
+
|
474
|
+
request.set_form_data(Dropbox::clean_params(params))
|
475
|
+
|
476
|
+
response = Dropbox::do_http(uri, request)
|
477
|
+
|
478
|
+
j = Dropbox::parse_response(response)
|
479
|
+
["token_type", "access_token", "uid"].each { |k|
|
480
|
+
if not j.has_key?(k)
|
481
|
+
raise DropboxError.new("Bad response from /token: missing \"#{k}\".")
|
482
|
+
end
|
483
|
+
if not j[k].is_a?(String)
|
484
|
+
raise DropboxError.new("Bad response from /token: field \"#{k}\" is not a string.")
|
485
|
+
end
|
486
|
+
}
|
487
|
+
if j["token_type"] != "bearer" and j["token_type"] != "Bearer"
|
488
|
+
raise DropboxError.new("Bad response from /token: \"token_type\" is \"#{token_type}\".")
|
439
489
|
end
|
440
490
|
|
441
|
-
|
442
|
-
|
443
|
-
def _finish(code, original_redirect_uri)
|
444
|
-
if not code.is_a?(String)
|
445
|
-
raise ArgumentError, "code must be a String"
|
446
|
-
end
|
447
|
-
|
448
|
-
uri = URI.parse("https://#{Dropbox::API_SERVER}/1/oauth2/token")
|
449
|
-
request = Net::HTTP::Post.new(uri.request_uri)
|
450
|
-
client_credentials = @consumer_key + ':' + @consumer_secret
|
451
|
-
request.add_field('Authorization', 'Basic ' + Base64.encode64(client_credentials).chomp("\n"))
|
452
|
-
|
453
|
-
params = {
|
454
|
-
"grant_type" => "authorization_code",
|
455
|
-
"code" => code,
|
456
|
-
"redirect_uri" => original_redirect_uri,
|
457
|
-
"locale" => @locale,
|
458
|
-
}
|
459
|
-
|
460
|
-
request.set_form_data(Dropbox::clean_params(params))
|
461
|
-
|
462
|
-
response = Dropbox::do_http(uri, request)
|
463
|
-
|
464
|
-
j = Dropbox::parse_response(response)
|
465
|
-
["token_type", "access_token", "uid"].each { |k|
|
466
|
-
if not j.has_key?(k)
|
467
|
-
raise DropboxError.new("Bad response from /token: missing \"#{k}\".")
|
468
|
-
end
|
469
|
-
if not j[k].is_a?(String)
|
470
|
-
raise DropboxError.new("Bad response from /token: field \"#{k}\" is not a string.")
|
471
|
-
end
|
472
|
-
}
|
473
|
-
if j["token_type"] != "bearer" and j["token_type"] != "Bearer"
|
474
|
-
raise DropboxError.new("Bad response from /token: \"token_type\" is \"#{token_type}\".")
|
475
|
-
end
|
476
|
-
|
477
|
-
return j['access_token'], j['uid']
|
478
|
-
end
|
491
|
+
return j['access_token'], j['uid']
|
492
|
+
end
|
479
493
|
end
|
480
494
|
|
481
495
|
# OAuth 2 authorization helper for apps that can't provide a redirect URI
|
482
496
|
# (such as the command line example apps).
|
483
497
|
class DropboxOAuth2FlowNoRedirect < DropboxOAuth2FlowBase
|
484
498
|
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
499
|
+
# * consumer_key: Your Dropbox API app's "app key"
|
500
|
+
# * consumer_secret: Your Dropbox API app's "app secret"
|
501
|
+
# * locale: The locale of the user currently using your app.
|
502
|
+
def initialize(consumer_key, consumer_secret, locale=nil)
|
503
|
+
super(consumer_key, consumer_secret, locale)
|
504
|
+
end
|
505
|
+
|
506
|
+
# Returns a authorization_url, which is a page on Dropbox's website. Have the user
|
507
|
+
# visit this URL and approve your app.
|
508
|
+
def start()
|
509
|
+
_get_authorize_url(nil, nil)
|
510
|
+
end
|
511
|
+
|
512
|
+
# If the user approves your app, they will be presented with an "authorization code".
|
513
|
+
# Have the user copy/paste that authorization code into your app and then call this
|
514
|
+
# method to get an access token.
|
515
|
+
#
|
516
|
+
# Returns a two-entry list (access_token, user_id)
|
517
|
+
# * access_token is an access token string that can be passed to DropboxClient.
|
518
|
+
# * user_id is the Dropbox user ID of the user that just approved your app.
|
519
|
+
def finish(code)
|
520
|
+
_finish(code, nil)
|
521
|
+
end
|
508
522
|
end
|
509
523
|
|
510
524
|
# The standard OAuth 2 authorization helper. Use this if you're writing a web app.
|
511
525
|
class DropboxOAuth2Flow < DropboxOAuth2FlowBase
|
512
526
|
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
527
|
+
# * consumer_key: Your Dropbox API app's "app key"
|
528
|
+
# * consumer_secret: Your Dropbox API app's "app secret"
|
529
|
+
# * redirect_uri: The URI that the Dropbox server will redirect the user to after the user
|
530
|
+
# finishes authorizing your app. This URI must be HTTPs-based and pre-registered with
|
531
|
+
# the Dropbox servers, though localhost URIs are allowed without pre-registration and can
|
532
|
+
# be either HTTP or HTTPS.
|
533
|
+
# * session: A hash that represents the current web app session (will be used to save the CSRF
|
534
|
+
# token)
|
535
|
+
# * csrf_token_key: The key to use when storing the CSRF token in the session (for example,
|
536
|
+
# :dropbox_auth_csrf_token)
|
537
|
+
# * locale: The locale of the user currently using your app (ex: "en" or "en_US").
|
538
|
+
def initialize(consumer_key, consumer_secret, redirect_uri, session, csrf_token_session_key, locale=nil)
|
539
|
+
super(consumer_key, consumer_secret, locale)
|
540
|
+
if not redirect_uri.is_a?(String)
|
541
|
+
raise ArgumentError, "redirect_uri must be a String, got #{consumer_secret.inspect}"
|
542
|
+
end
|
543
|
+
@redirect_uri = redirect_uri
|
544
|
+
@session = session
|
545
|
+
@csrf_token_session_key = csrf_token_session_key
|
546
|
+
end
|
547
|
+
|
548
|
+
# Starts the OAuth 2 authorizaton process, which involves redirecting the user to
|
549
|
+
# the returned "authorization URL" (a URL on the Dropbox website). When the user then
|
550
|
+
# either approves or denies your app access, Dropbox will redirect them to the
|
551
|
+
# redirect_uri you provided to the constructor, at which point you should call finish()
|
552
|
+
# to complete the process.
|
553
|
+
#
|
554
|
+
# This function will also save a CSRF token to the session and csrf_token_session_key
|
555
|
+
# you provided to the constructor. This CSRF token will be checked on finish() to prevent
|
556
|
+
# request forgery.
|
557
|
+
#
|
558
|
+
# * url_state: Any data you would like to keep in the URL through the authorization
|
559
|
+
# process. This exact value will be returned to you by finish().
|
560
|
+
#
|
561
|
+
# Returns the URL to redirect the user to.
|
562
|
+
def start(url_state=nil)
|
563
|
+
unless url_state.nil? or url_state.is_a?(String)
|
564
|
+
raise ArgumentError, "url_state must be a String"
|
565
|
+
end
|
566
|
+
|
567
|
+
csrf_token = SecureRandom.base64(16)
|
568
|
+
state = csrf_token
|
569
|
+
unless url_state.nil?
|
570
|
+
state += "|" + url_state
|
571
|
+
end
|
572
|
+
@session[@csrf_token_session_key] = csrf_token
|
573
|
+
|
574
|
+
return _get_authorize_url(@redirect_uri, state)
|
575
|
+
end
|
576
|
+
|
577
|
+
# Call this after the user has visited the authorize URL (see: start()), approved your app,
|
578
|
+
# and was redirected to your redirect URI.
|
579
|
+
#
|
580
|
+
# * query_params: The query params on the GET request to your redirect URI.
|
581
|
+
#
|
582
|
+
# Returns a tuple of (access_token, user_id, url_state). access_token can be used to
|
583
|
+
# construct a DropboxClient. user_id is the Dropbox user ID of the user that jsut approved
|
584
|
+
# your app. url_state is the value you originally passed in to start().
|
585
|
+
#
|
586
|
+
# Can throw BadRequestError, BadStateError, CsrfError, NotApprovedError,
|
587
|
+
# ProviderError, and the standard DropboxError.
|
588
|
+
def finish(query_params)
|
589
|
+
csrf_token_from_session = @session[@csrf_token_session_key]
|
590
|
+
|
591
|
+
# Check well-formedness of request.
|
592
|
+
|
593
|
+
state = query_params['state']
|
594
|
+
if state.nil?
|
595
|
+
raise BadRequestError.new("Missing query parameter 'state'.")
|
596
|
+
end
|
597
|
+
|
598
|
+
error = query_params['error']
|
599
|
+
error_description = query_params['error_description']
|
600
|
+
code = query_params['code']
|
601
|
+
|
602
|
+
if not error.nil? and not code.nil?
|
603
|
+
raise BadRequestError.new("Query parameters 'code' and 'error' are both set;" +
|
604
|
+
" only one must be set.")
|
605
|
+
end
|
606
|
+
if error.nil? and code.nil?
|
607
|
+
raise BadRequestError.new("Neither query parameter 'code' or 'error' is set.")
|
608
|
+
end
|
609
|
+
|
610
|
+
# Check CSRF token
|
611
|
+
|
612
|
+
if csrf_token_from_session.nil?
|
613
|
+
raise BadStateError.new("Missing CSRF token in session.");
|
614
|
+
end
|
615
|
+
unless csrf_token_from_session.length > 20
|
616
|
+
raise RuntimeError.new("CSRF token unexpectedly short: #{csrf_token_from_session.inspect}")
|
617
|
+
end
|
618
|
+
|
619
|
+
split_pos = state.index('|')
|
620
|
+
if split_pos.nil?
|
621
|
+
given_csrf_token = state
|
622
|
+
url_state = nil
|
623
|
+
else
|
624
|
+
given_csrf_token, url_state = state.split('|', 2)
|
625
|
+
end
|
626
|
+
if not Dropbox::safe_string_equals(csrf_token_from_session, given_csrf_token)
|
627
|
+
raise CsrfError.new("Expected #{csrf_token_from_session.inspect}, " +
|
628
|
+
"got #{given_csrf_token.inspect}.")
|
629
|
+
end
|
630
|
+
@session.delete(@csrf_token_session_key)
|
631
|
+
|
632
|
+
# Check for error identifier
|
633
|
+
|
634
|
+
if not error.nil?
|
635
|
+
if error == 'access_denied'
|
636
|
+
# The user clicked "Deny"
|
637
|
+
if error_description.nil?
|
638
|
+
raise NotApprovedError.new("No additional description from Dropbox.")
|
609
639
|
else
|
610
|
-
|
611
|
-
end
|
612
|
-
if not Dropbox::safe_string_equals(csrf_token_from_session, given_csrf_token)
|
613
|
-
raise CsrfError.new("Expected #{csrf_token_from_session.inspect}, " +
|
614
|
-
"got #{given_csrf_token.inspect}.")
|
640
|
+
raise NotApprovedError.new("Additional description from Dropbox: #{error_description}")
|
615
641
|
end
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
if error == 'access_denied'
|
622
|
-
# The user clicked "Deny"
|
623
|
-
if error_description.nil?
|
624
|
-
raise NotApprovedError.new("No additional description from Dropbox.")
|
625
|
-
else
|
626
|
-
raise NotApprovedError.new("Additional description from Dropbox: #{error_description}")
|
627
|
-
end
|
628
|
-
else
|
629
|
-
# All other errors.
|
630
|
-
full_message = error
|
631
|
-
if not error_description.nil?
|
632
|
-
full_message += ": " + error_description
|
633
|
-
end
|
634
|
-
raise ProviderError.new(full_message)
|
635
|
-
end
|
642
|
+
else
|
643
|
+
# All other errors.
|
644
|
+
full_message = error
|
645
|
+
if not error_description.nil?
|
646
|
+
full_message += ": " + error_description
|
636
647
|
end
|
648
|
+
raise ProviderError.new(full_message)
|
649
|
+
end
|
650
|
+
end
|
637
651
|
|
638
|
-
|
652
|
+
# If everything went ok, make the network call to get an access token.
|
639
653
|
|
640
|
-
|
641
|
-
|
642
|
-
|
654
|
+
access_token, user_id = _finish(code, @redirect_uri)
|
655
|
+
return access_token, user_id, url_state
|
656
|
+
end
|
643
657
|
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
658
|
+
# Thrown if the redirect URL was missing parameters or if the given parameters were not valid.
|
659
|
+
#
|
660
|
+
# The recommended action is to show an HTTP 400 error page.
|
661
|
+
class BadRequestError < Exception; end
|
648
662
|
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
663
|
+
# Thrown if all the parameters are correct, but there's no CSRF token in the session. This
|
664
|
+
# probably means that the session expired.
|
665
|
+
#
|
666
|
+
# The recommended action is to redirect the user's browser to try the approval process again.
|
667
|
+
class BadStateError < Exception; end
|
654
668
|
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
669
|
+
# Thrown if the given 'state' parameter doesn't contain the CSRF token from the user's session.
|
670
|
+
# This is blocked to prevent CSRF attacks.
|
671
|
+
#
|
672
|
+
# The recommended action is to respond with an HTTP 403 error page.
|
673
|
+
class CsrfError < Exception; end
|
660
674
|
|
661
|
-
|
662
|
-
|
675
|
+
# The user chose not to approve your app.
|
676
|
+
class NotApprovedError < Exception; end
|
663
677
|
|
664
|
-
|
665
|
-
|
666
|
-
|
678
|
+
# Dropbox redirected to your redirect URI with some unexpected error identifier and error
|
679
|
+
# message.
|
680
|
+
class ProviderError < Exception; end
|
667
681
|
end
|
668
682
|
|
669
683
|
|
670
684
|
# A class that represents either an OAuth request token or an OAuth access token.
|
671
685
|
class OAuthToken # :nodoc:
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
def key
|
678
|
-
@key
|
679
|
-
end
|
680
|
-
|
681
|
-
def secret
|
682
|
-
@secret
|
683
|
-
end
|
686
|
+
attr_reader :key, :secret
|
687
|
+
def initialize(key, secret)
|
688
|
+
@key = key
|
689
|
+
@secret = secret
|
690
|
+
end
|
684
691
|
end
|
685
692
|
|
686
693
|
|
687
694
|
# This is the usual error raised on any Dropbox related Errors
|
688
695
|
class DropboxError < RuntimeError
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
696
|
+
attr_accessor :http_response, :error, :user_error
|
697
|
+
def initialize(error, http_response=nil, user_error=nil)
|
698
|
+
@error = error
|
699
|
+
@http_response = http_response
|
700
|
+
@user_error = user_error
|
701
|
+
end
|
702
|
+
|
703
|
+
def to_s
|
704
|
+
return "#{user_error} (#{error})" if user_error
|
705
|
+
"#{error}"
|
706
|
+
end
|
700
707
|
end
|
701
708
|
|
702
709
|
# This is the error raised on Authentication failures. Usually this means
|
@@ -716,77 +723,183 @@ end
|
|
716
723
|
# first; you can get one using either DropboxOAuth2Flow or DropboxOAuth2FlowNoRedirect.
|
717
724
|
class DropboxClient
|
718
725
|
|
726
|
+
# Args:
|
727
|
+
# * +oauth2_access_token+: Obtained via DropboxOAuth2Flow or DropboxOAuth2FlowNoRedirect.
|
728
|
+
# * +locale+: The user's current locale (used to localize error messages).
|
729
|
+
def initialize(oauth2_access_token, root="auto", locale=nil)
|
730
|
+
if oauth2_access_token.is_a?(String)
|
731
|
+
@session = DropboxOAuth2Session.new(oauth2_access_token, locale)
|
732
|
+
elsif oauth2_access_token.is_a?(DropboxSession)
|
733
|
+
@session = oauth2_access_token
|
734
|
+
@session.get_access_token
|
735
|
+
if not locale.nil?
|
736
|
+
@session.locale = locale
|
737
|
+
end
|
738
|
+
else
|
739
|
+
raise ArgumentError.new("oauth2_access_token doesn't have a valid type")
|
740
|
+
end
|
741
|
+
|
742
|
+
@root = root.to_s # If they passed in a symbol, make it a string
|
743
|
+
|
744
|
+
if not ["dropbox","app_folder","auto"].include?(@root)
|
745
|
+
raise ArgumentError.new("root must be :dropbox, :app_folder, or :auto")
|
746
|
+
end
|
747
|
+
if @root == "app_folder"
|
748
|
+
#App Folder is the name of the access type, but for historical reasons
|
749
|
+
#sandbox is the URL root component that indicates this
|
750
|
+
@root = "sandbox"
|
751
|
+
end
|
752
|
+
end
|
753
|
+
|
754
|
+
# Returns some information about the current user's Dropbox account (the "current user"
|
755
|
+
# is the user associated with the access token you're using).
|
756
|
+
#
|
757
|
+
# For a detailed description of what this call returns, visit:
|
758
|
+
# https://www.dropbox.com/developers/reference/api#account-info
|
759
|
+
def account_info()
|
760
|
+
response = @session.do_get "/account/info"
|
761
|
+
Dropbox::parse_response(response)
|
762
|
+
end
|
763
|
+
|
764
|
+
# Disables the access token that this +DropboxClient+ is using. If this call
|
765
|
+
# succeeds, further API calls using this object will fail.
|
766
|
+
def disable_access_token
|
767
|
+
@session.do_post "/disable_access_token"
|
768
|
+
nil
|
769
|
+
end
|
770
|
+
|
771
|
+
# If this +DropboxClient+ was created with an OAuth 1 access token, this method
|
772
|
+
# can be used to create an equivalent OAuth 2 access token. This can be used to
|
773
|
+
# upgrade your app's existing access tokens from OAuth 1 to OAuth 2.
|
774
|
+
def create_oauth2_access_token
|
775
|
+
if not @session.is_a?(DropboxSession)
|
776
|
+
raise ArgumentError.new("This call requires a DropboxClient that is configured with " \
|
777
|
+
"an OAuth 1 access token.")
|
778
|
+
end
|
779
|
+
response = @session.do_post "/oauth2/token_from_oauth1"
|
780
|
+
Dropbox::parse_response(response)['access_token']
|
781
|
+
end
|
782
|
+
|
783
|
+
# Uploads a file to a server. This uses the HTTP PUT upload method for simplicity
|
784
|
+
#
|
785
|
+
# Args:
|
786
|
+
# * +to_path+: The directory path to upload the file to. If the destination
|
787
|
+
# directory does not yet exist, it will be created.
|
788
|
+
# * +file_obj+: A file-like object to upload. If you would like, you can
|
789
|
+
# pass a string as file_obj.
|
790
|
+
# * +overwrite+: Whether to overwrite an existing file at the given path. [default is False]
|
791
|
+
# If overwrite is False and a file already exists there, Dropbox
|
792
|
+
# will rename the upload to make sure it doesn't overwrite anything.
|
793
|
+
# You must check the returned metadata to know what this new name is.
|
794
|
+
# This field should only be True if your intent is to potentially
|
795
|
+
# clobber changes to a file that you don't know about.
|
796
|
+
# * +parent_rev+: The rev field from the 'parent' of this upload. [optional]
|
797
|
+
# If your intent is to update the file at the given path, you should
|
798
|
+
# pass the parent_rev parameter set to the rev value from the most recent
|
799
|
+
# metadata you have of the existing file at that path. If the server
|
800
|
+
# has a more recent version of the file at the specified path, it will
|
801
|
+
# automatically rename your uploaded file, spinning off a conflict.
|
802
|
+
# Using this parameter effectively causes the overwrite parameter to be ignored.
|
803
|
+
# The file will always be overwritten if you send the most-recent parent_rev,
|
804
|
+
# and it will never be overwritten you send a less-recent one.
|
805
|
+
# Returns:
|
806
|
+
# * a Hash containing the metadata of the newly uploaded file. The file may have a different
|
807
|
+
# name if it conflicted.
|
808
|
+
#
|
809
|
+
# Simple Example
|
810
|
+
# client = DropboxClient(oauth2_access_token)
|
811
|
+
# #session is a DropboxSession I've already authorized
|
812
|
+
# client.put_file('/test_file_on_dropbox', open('/tmp/test_file'))
|
813
|
+
# This will upload the "/tmp/test_file" from my computer into the root of my App's app folder
|
814
|
+
# and call it "test_file_on_dropbox".
|
815
|
+
# The file will not overwrite any pre-existing file.
|
816
|
+
def put_file(to_path, file_obj, overwrite=false, parent_rev=nil)
|
817
|
+
path = "/files_put/#{@root}#{format_path(to_path)}"
|
818
|
+
params = {
|
819
|
+
'overwrite' => overwrite.to_s,
|
820
|
+
'parent_rev' => parent_rev,
|
821
|
+
}
|
822
|
+
|
823
|
+
headers = {"Content-Type" => "application/octet-stream"}
|
824
|
+
response = @session.do_put path, params, headers, file_obj, :content
|
825
|
+
|
826
|
+
Dropbox::parse_response(response)
|
827
|
+
end
|
828
|
+
|
829
|
+
# Returns a ChunkedUploader object.
|
830
|
+
#
|
831
|
+
# Args:
|
832
|
+
# * +file_obj+: The file-like object to be uploaded. Must support .read()
|
833
|
+
# * +total_size+: The total size of file_obj
|
834
|
+
def get_chunked_uploader(file_obj, total_size)
|
835
|
+
ChunkedUploader.new(self, file_obj, total_size)
|
836
|
+
end
|
837
|
+
|
838
|
+
# ChunkedUploader is responsible for uploading a large file to Dropbox in smaller chunks.
|
839
|
+
# This allows large files to be uploaded and makes allows recovery during failure.
|
840
|
+
class ChunkedUploader
|
841
|
+
attr_accessor :file_obj, :total_size, :offset, :upload_id, :client
|
842
|
+
|
843
|
+
def initialize(client, file_obj, total_size)
|
844
|
+
@client = client
|
845
|
+
@file_obj = file_obj
|
846
|
+
@total_size = total_size
|
847
|
+
@upload_id = nil
|
848
|
+
@offset = 0
|
849
|
+
end
|
850
|
+
|
851
|
+
# Uploads data from this ChunkedUploader's file_obj in chunks, until
|
852
|
+
# an error occurs. Throws an exception when an error occurs, and can
|
853
|
+
# be called again to resume the upload.
|
854
|
+
#
|
719
855
|
# Args:
|
720
|
-
# * +
|
721
|
-
|
722
|
-
|
723
|
-
if oauth2_access_token.is_a?(String)
|
724
|
-
@session = DropboxOAuth2Session.new(oauth2_access_token, locale)
|
725
|
-
elsif oauth2_access_token.is_a?(DropboxSession)
|
726
|
-
@session = oauth2_access_token
|
727
|
-
@session.get_access_token
|
728
|
-
if not locale.nil?
|
729
|
-
@session.locale = locale
|
730
|
-
end
|
731
|
-
else
|
732
|
-
raise ArgumentError.new("oauth2_access_token doesn't have a valid type")
|
733
|
-
end
|
856
|
+
# * +chunk_size+: The chunk size for each individual upload. Defaults to 4MB.
|
857
|
+
def upload(chunk_size=4*1024*1024)
|
858
|
+
last_chunk = nil
|
734
859
|
|
735
|
-
|
736
|
-
|
737
|
-
|
738
|
-
raise ArgumentError.new("root must be :dropbox, :app_folder, or :auto")
|
739
|
-
end
|
740
|
-
if @root == "app_folder"
|
741
|
-
#App Folder is the name of the access type, but for historical reasons
|
742
|
-
#sandbox is the URL root component that indicates this
|
743
|
-
@root = "sandbox"
|
860
|
+
while @offset < @total_size
|
861
|
+
if not last_chunk
|
862
|
+
last_chunk = @file_obj.read(chunk_size)
|
744
863
|
end
|
745
|
-
end
|
746
864
|
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
|
865
|
+
resp = {}
|
866
|
+
begin
|
867
|
+
resp = Dropbox::parse_response(@client.partial_chunked_upload(last_chunk, @upload_id, @offset))
|
868
|
+
last_chunk = nil
|
869
|
+
rescue SocketError => e
|
870
|
+
raise e
|
871
|
+
rescue SystemCallError => e
|
872
|
+
raise e
|
873
|
+
rescue DropboxError => e
|
874
|
+
raise e if e.http_response.nil? or e.http_response.code[0] == '5'
|
875
|
+
begin
|
876
|
+
resp = JSON.parse(e.http_response.body)
|
877
|
+
raise DropboxError.new('server response does not have offset key') unless resp.has_key? 'offset'
|
878
|
+
rescue JSON::ParserError
|
879
|
+
raise DropboxError.new("Unable to parse JSON response: #{e.http_response.body}")
|
880
|
+
end
|
881
|
+
end
|
763
882
|
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
def create_oauth2_access_token
|
768
|
-
if not @session.is_a?(DropboxSession)
|
769
|
-
raise ArgumentError.new("This call requires a DropboxClient that is configured with " \
|
770
|
-
"an OAuth 1 access token.")
|
883
|
+
if resp.has_key? 'offset' and resp['offset'] > @offset
|
884
|
+
@offset += (resp['offset'] - @offset) if resp['offset']
|
885
|
+
last_chunk = nil
|
771
886
|
end
|
772
|
-
|
773
|
-
|
887
|
+
@upload_id = resp['upload_id'] if resp['upload_id']
|
888
|
+
end
|
774
889
|
end
|
775
890
|
|
776
|
-
#
|
891
|
+
# Completes a file upload
|
777
892
|
#
|
778
893
|
# Args:
|
779
894
|
# * +to_path+: The directory path to upload the file to. If the destination
|
780
895
|
# directory does not yet exist, it will be created.
|
781
|
-
# * +file_obj+: A file-like object to upload. If you would like, you can
|
782
|
-
# pass a string as file_obj.
|
783
896
|
# * +overwrite+: Whether to overwrite an existing file at the given path. [default is False]
|
784
897
|
# If overwrite is False and a file already exists there, Dropbox
|
785
898
|
# will rename the upload to make sure it doesn't overwrite anything.
|
786
899
|
# You must check the returned metadata to know what this new name is.
|
787
900
|
# This field should only be True if your intent is to potentially
|
788
901
|
# clobber changes to a file that you don't know about.
|
789
|
-
# *
|
902
|
+
# * parent_rev: The rev field from the 'parent' of this upload.
|
790
903
|
# If your intent is to update the file at the given path, you should
|
791
904
|
# pass the parent_rev parameter set to the rev value from the most recent
|
792
905
|
# metadata you have of the existing file at that path. If the server
|
@@ -795,595 +908,527 @@ class DropboxClient
|
|
795
908
|
# Using this parameter effectively causes the overwrite parameter to be ignored.
|
796
909
|
# The file will always be overwritten if you send the most-recent parent_rev,
|
797
910
|
# and it will never be overwritten you send a less-recent one.
|
798
|
-
# Returns:
|
799
|
-
# * a Hash containing the metadata of the newly uploaded file. The file may have a different
|
800
|
-
# name if it conflicted.
|
801
|
-
#
|
802
|
-
# Simple Example
|
803
|
-
# client = DropboxClient(oauth2_access_token)
|
804
|
-
# #session is a DropboxSession I've already authorized
|
805
|
-
# client.put_file('/test_file_on_dropbox', open('/tmp/test_file'))
|
806
|
-
# This will upload the "/tmp/test_file" from my computer into the root of my App's app folder
|
807
|
-
# and call it "test_file_on_dropbox".
|
808
|
-
# The file will not overwrite any pre-existing file.
|
809
|
-
def put_file(to_path, file_obj, overwrite=false, parent_rev=nil)
|
810
|
-
path = "/files_put/#{@root}#{format_path(to_path)}"
|
811
|
-
params = {
|
812
|
-
'overwrite' => overwrite.to_s,
|
813
|
-
'parent_rev' => parent_rev,
|
814
|
-
}
|
815
|
-
|
816
|
-
headers = {"Content-Type" => "application/octet-stream"}
|
817
|
-
content_server = true
|
818
|
-
response = @session.do_put path, params, headers, file_obj, content_server
|
819
|
-
|
820
|
-
Dropbox::parse_response(response)
|
821
|
-
end
|
822
|
-
|
823
|
-
# Returns a ChunkedUploader object.
|
824
|
-
#
|
825
|
-
# Args:
|
826
|
-
# * +file_obj+: The file-like object to be uploaded. Must support .read()
|
827
|
-
# * +total_size+: The total size of file_obj
|
828
|
-
def get_chunked_uploader(file_obj, total_size)
|
829
|
-
ChunkedUploader.new(self, file_obj, total_size)
|
830
|
-
end
|
831
|
-
|
832
|
-
# ChunkedUploader is responsible for uploading a large file to Dropbox in smaller chunks.
|
833
|
-
# This allows large files to be uploaded and makes allows recovery during failure.
|
834
|
-
class ChunkedUploader
|
835
|
-
attr_accessor :file_obj, :total_size, :offset, :upload_id, :client
|
836
|
-
|
837
|
-
def initialize(client, file_obj, total_size)
|
838
|
-
@client = client
|
839
|
-
@file_obj = file_obj
|
840
|
-
@total_size = total_size
|
841
|
-
@upload_id = nil
|
842
|
-
@offset = 0
|
843
|
-
end
|
844
|
-
|
845
|
-
# Uploads data from this ChunkedUploader's file_obj in chunks, until
|
846
|
-
# an error occurs. Throws an exception when an error occurs, and can
|
847
|
-
# be called again to resume the upload.
|
848
|
-
#
|
849
|
-
# Args:
|
850
|
-
# * +chunk_size+: The chunk size for each individual upload. Defaults to 4MB.
|
851
|
-
def upload(chunk_size=4*1024*1024)
|
852
|
-
last_chunk = nil
|
853
|
-
|
854
|
-
while @offset < @total_size
|
855
|
-
if not last_chunk
|
856
|
-
last_chunk = @file_obj.read(chunk_size)
|
857
|
-
end
|
858
|
-
|
859
|
-
resp = {}
|
860
|
-
begin
|
861
|
-
resp = Dropbox::parse_response(@client.partial_chunked_upload(last_chunk, @upload_id, @offset))
|
862
|
-
last_chunk = nil
|
863
|
-
rescue SocketError => e
|
864
|
-
raise e
|
865
|
-
rescue SystemCallError => e
|
866
|
-
raise e
|
867
|
-
rescue DropboxError => e
|
868
|
-
raise e if e.http_response.nil? or e.http_response.code[0] == '5'
|
869
|
-
begin
|
870
|
-
resp = JSON.parse(e.http_response.body)
|
871
|
-
raise DropboxError.new('server response does not have offset key') unless resp.has_key? 'offset'
|
872
|
-
rescue JSON::ParserError
|
873
|
-
raise DropboxError.new("Unable to parse JSON response: #{e.http_response.body}")
|
874
|
-
end
|
875
|
-
end
|
876
|
-
|
877
|
-
if resp.has_key? 'offset' and resp['offset'] > @offset
|
878
|
-
@offset += (resp['offset'] - @offset) if resp['offset']
|
879
|
-
last_chunk = nil
|
880
|
-
end
|
881
|
-
@upload_id = resp['upload_id'] if resp['upload_id']
|
882
|
-
end
|
883
|
-
end
|
884
|
-
|
885
|
-
# Completes a file upload
|
886
|
-
#
|
887
|
-
# Args:
|
888
|
-
# * +to_path+: The directory path to upload the file to. If the destination
|
889
|
-
# directory does not yet exist, it will be created.
|
890
|
-
# * +overwrite+: Whether to overwrite an existing file at the given path. [default is False]
|
891
|
-
# If overwrite is False and a file already exists there, Dropbox
|
892
|
-
# will rename the upload to make sure it doesn't overwrite anything.
|
893
|
-
# You must check the returned metadata to know what this new name is.
|
894
|
-
# This field should only be True if your intent is to potentially
|
895
|
-
# clobber changes to a file that you don't know about.
|
896
|
-
# * parent_rev: The rev field from the 'parent' of this upload.
|
897
|
-
# If your intent is to update the file at the given path, you should
|
898
|
-
# pass the parent_rev parameter set to the rev value from the most recent
|
899
|
-
# metadata you have of the existing file at that path. If the server
|
900
|
-
# has a more recent version of the file at the specified path, it will
|
901
|
-
# automatically rename your uploaded file, spinning off a conflict.
|
902
|
-
# Using this parameter effectively causes the overwrite parameter to be ignored.
|
903
|
-
# The file will always be overwritten if you send the most-recent parent_rev,
|
904
|
-
# and it will never be overwritten you send a less-recent one.
|
905
|
-
#
|
906
|
-
# Returns:
|
907
|
-
# * A Hash with the metadata of file just uploaded.
|
908
|
-
# For a detailed description of what this call returns, visit:
|
909
|
-
# https://www.dropbox.com/developers/reference/api#metadata
|
910
|
-
def finish(to_path, overwrite=false, parent_rev=nil)
|
911
|
-
response = @client.commit_chunked_upload(to_path, @upload_id, overwrite, parent_rev)
|
912
|
-
Dropbox::parse_response(response)
|
913
|
-
end
|
914
|
-
end
|
915
|
-
|
916
|
-
def commit_chunked_upload(to_path, upload_id, overwrite=false, parent_rev=nil) #:nodoc
|
917
|
-
path = "/commit_chunked_upload/#{@root}#{format_path(to_path)}"
|
918
|
-
params = {'overwrite' => overwrite.to_s,
|
919
|
-
'upload_id' => upload_id,
|
920
|
-
'parent_rev' => parent_rev,
|
921
|
-
}
|
922
|
-
headers = nil
|
923
|
-
content_server = true
|
924
|
-
@session.do_post path, params, headers, content_server
|
925
|
-
end
|
926
|
-
|
927
|
-
def partial_chunked_upload(data, upload_id=nil, offset=nil) #:nodoc
|
928
|
-
params = {
|
929
|
-
'upload_id' => upload_id,
|
930
|
-
'offset' => offset,
|
931
|
-
}
|
932
|
-
headers = {'Content-Type' => "application/octet-stream"}
|
933
|
-
content_server = true
|
934
|
-
@session.do_put '/chunked_upload', params, headers, data, content_server
|
935
|
-
end
|
936
|
-
|
937
|
-
# Download a file
|
938
|
-
#
|
939
|
-
# Args:
|
940
|
-
# * +from_path+: The path to the file to be downloaded
|
941
|
-
# * +rev+: A previous revision value of the file to be downloaded
|
942
|
-
#
|
943
|
-
# Returns:
|
944
|
-
# * The file contents.
|
945
|
-
def get_file(from_path, rev=nil)
|
946
|
-
response = get_file_impl(from_path, rev)
|
947
|
-
Dropbox::parse_response(response, raw=true)
|
948
|
-
end
|
949
|
-
|
950
|
-
# Download a file and get its metadata.
|
951
|
-
#
|
952
|
-
# Args:
|
953
|
-
# * +from_path+: The path to the file to be downloaded
|
954
|
-
# * +rev+: A previous revision value of the file to be downloaded
|
955
|
-
#
|
956
|
-
# Returns:
|
957
|
-
# * The file contents.
|
958
|
-
# * The file metadata as a hash.
|
959
|
-
def get_file_and_metadata(from_path, rev=nil)
|
960
|
-
response = get_file_impl(from_path, rev)
|
961
|
-
parsed_response = Dropbox::parse_response(response, raw=true)
|
962
|
-
metadata = parse_metadata(response)
|
963
|
-
return parsed_response, metadata
|
964
|
-
end
|
965
|
-
|
966
|
-
# Download a file (helper method - don't call this directly).
|
967
|
-
#
|
968
|
-
# Args:
|
969
|
-
# * +from_path+: The path to the file to be downloaded
|
970
|
-
# * +rev+: A previous revision value of the file to be downloaded
|
971
|
-
#
|
972
|
-
# Returns:
|
973
|
-
# * The HTTPResponse for the file download request.
|
974
|
-
def get_file_impl(from_path, rev=nil) # :nodoc:
|
975
|
-
path = "/files/#{@root}#{format_path(from_path)}"
|
976
|
-
params = {
|
977
|
-
'rev' => rev,
|
978
|
-
}
|
979
|
-
headers = nil
|
980
|
-
content_server = true
|
981
|
-
@session.do_get path, params, headers, content_server
|
982
|
-
end
|
983
|
-
private :get_file_impl
|
984
|
-
|
985
|
-
# Parses out file metadata from a raw dropbox HTTP response.
|
986
|
-
#
|
987
|
-
# Args:
|
988
|
-
# * +dropbox_raw_response+: The raw, unparsed HTTPResponse from Dropbox.
|
989
|
-
#
|
990
|
-
# Returns:
|
991
|
-
# * The metadata of the file as a hash.
|
992
|
-
def parse_metadata(dropbox_raw_response) # :nodoc:
|
993
|
-
begin
|
994
|
-
raw_metadata = dropbox_raw_response['x-dropbox-metadata']
|
995
|
-
metadata = JSON.parse(raw_metadata)
|
996
|
-
rescue
|
997
|
-
raise DropboxError.new("Dropbox Server Error: x-dropbox-metadata=#{raw_metadata}",
|
998
|
-
dropbox_raw_response)
|
999
|
-
end
|
1000
|
-
return metadata
|
1001
|
-
end
|
1002
|
-
private :parse_metadata
|
1003
|
-
|
1004
|
-
# Copy a file or folder to a new location.
|
1005
|
-
#
|
1006
|
-
# Args:
|
1007
|
-
# * +from_path+: The path to the file or folder to be copied.
|
1008
|
-
# * +to_path+: The destination path of the file or folder to be copied.
|
1009
|
-
# This parameter should include the destination filename (e.g.
|
1010
|
-
# from_path: '/test.txt', to_path: '/dir/test.txt'). If there's
|
1011
|
-
# already a file at the to_path, this copy will be renamed to
|
1012
|
-
# be unique.
|
1013
|
-
#
|
1014
|
-
# Returns:
|
1015
|
-
# * A hash with the metadata of the new copy of the file or folder.
|
1016
|
-
# For a detailed description of what this call returns, visit:
|
1017
|
-
# https://www.dropbox.com/developers/reference/api#fileops-copy
|
1018
|
-
def file_copy(from_path, to_path)
|
1019
|
-
params = {
|
1020
|
-
"root" => @root,
|
1021
|
-
"from_path" => format_path(from_path, false),
|
1022
|
-
"to_path" => format_path(to_path, false),
|
1023
|
-
}
|
1024
|
-
response = @session.do_post "/fileops/copy", params
|
1025
|
-
Dropbox::parse_response(response)
|
1026
|
-
end
|
1027
|
-
|
1028
|
-
# Create a folder.
|
1029
|
-
#
|
1030
|
-
# Arguments:
|
1031
|
-
# * +path+: The path of the new folder.
|
1032
|
-
#
|
1033
|
-
# Returns:
|
1034
|
-
# * A hash with the metadata of the newly created folder.
|
1035
|
-
# For a detailed description of what this call returns, visit:
|
1036
|
-
# https://www.dropbox.com/developers/reference/api#fileops-create-folder
|
1037
|
-
def file_create_folder(path)
|
1038
|
-
params = {
|
1039
|
-
"root" => @root,
|
1040
|
-
"path" => format_path(path, false),
|
1041
|
-
}
|
1042
|
-
response = @session.do_post "/fileops/create_folder", params
|
1043
|
-
|
1044
|
-
Dropbox::parse_response(response)
|
1045
|
-
end
|
1046
|
-
|
1047
|
-
# Deletes a file
|
1048
|
-
#
|
1049
|
-
# Arguments:
|
1050
|
-
# * +path+: The path of the file to delete
|
1051
|
-
#
|
1052
|
-
# Returns:
|
1053
|
-
# * A Hash with the metadata of file just deleted.
|
1054
|
-
# For a detailed description of what this call returns, visit:
|
1055
|
-
# https://www.dropbox.com/developers/reference/api#fileops-delete
|
1056
|
-
def file_delete(path)
|
1057
|
-
params = {
|
1058
|
-
"root" => @root,
|
1059
|
-
"path" => format_path(path, false),
|
1060
|
-
}
|
1061
|
-
response = @session.do_post "/fileops/delete", params
|
1062
|
-
Dropbox::parse_response(response)
|
1063
|
-
end
|
1064
|
-
|
1065
|
-
# Moves a file
|
1066
|
-
#
|
1067
|
-
# Arguments:
|
1068
|
-
# * +from_path+: The path of the file to be moved
|
1069
|
-
# * +to_path+: The destination path of the file or folder to be moved
|
1070
|
-
# If the file or folder already exists, it will be renamed to be unique.
|
1071
911
|
#
|
1072
912
|
# Returns:
|
1073
|
-
# * A Hash with the metadata of file
|
913
|
+
# * A Hash with the metadata of file just uploaded.
|
1074
914
|
# For a detailed description of what this call returns, visit:
|
1075
|
-
# https://www.dropbox.com/developers/reference/api#
|
1076
|
-
def
|
1077
|
-
|
1078
|
-
|
1079
|
-
|
1080
|
-
|
1081
|
-
|
1082
|
-
|
1083
|
-
|
1084
|
-
|
1085
|
-
|
1086
|
-
|
1087
|
-
|
1088
|
-
|
1089
|
-
|
1090
|
-
|
1091
|
-
|
1092
|
-
|
1093
|
-
|
1094
|
-
|
1095
|
-
|
1096
|
-
|
1097
|
-
|
1098
|
-
|
1099
|
-
|
1100
|
-
|
1101
|
-
|
1102
|
-
|
1103
|
-
|
1104
|
-
|
1105
|
-
|
1106
|
-
|
1107
|
-
|
1108
|
-
|
1109
|
-
|
1110
|
-
|
1111
|
-
|
1112
|
-
|
1113
|
-
|
1114
|
-
|
1115
|
-
|
1116
|
-
|
1117
|
-
|
1118
|
-
|
1119
|
-
|
1120
|
-
|
1121
|
-
|
1122
|
-
|
1123
|
-
|
1124
|
-
|
1125
|
-
|
1126
|
-
|
1127
|
-
|
1128
|
-
|
1129
|
-
|
1130
|
-
|
1131
|
-
|
1132
|
-
|
1133
|
-
|
1134
|
-
|
1135
|
-
|
1136
|
-
|
1137
|
-
|
1138
|
-
|
1139
|
-
|
1140
|
-
|
1141
|
-
|
1142
|
-
|
1143
|
-
|
1144
|
-
|
1145
|
-
|
1146
|
-
|
1147
|
-
|
1148
|
-
|
1149
|
-
|
1150
|
-
|
1151
|
-
|
1152
|
-
|
1153
|
-
|
1154
|
-
|
1155
|
-
|
1156
|
-
|
1157
|
-
|
1158
|
-
|
1159
|
-
|
1160
|
-
|
1161
|
-
|
1162
|
-
|
1163
|
-
|
1164
|
-
|
1165
|
-
|
1166
|
-
|
1167
|
-
|
1168
|
-
|
1169
|
-
|
1170
|
-
|
1171
|
-
|
1172
|
-
|
1173
|
-
|
1174
|
-
|
1175
|
-
|
1176
|
-
|
1177
|
-
|
1178
|
-
|
1179
|
-
|
1180
|
-
|
1181
|
-
|
1182
|
-
|
1183
|
-
|
1184
|
-
|
1185
|
-
|
1186
|
-
|
1187
|
-
|
1188
|
-
|
1189
|
-
|
1190
|
-
|
1191
|
-
|
1192
|
-
|
1193
|
-
|
1194
|
-
|
1195
|
-
|
1196
|
-
|
1197
|
-
|
1198
|
-
#
|
1199
|
-
|
1200
|
-
|
1201
|
-
|
1202
|
-
|
1203
|
-
|
1204
|
-
|
1205
|
-
|
1206
|
-
|
1207
|
-
|
1208
|
-
|
1209
|
-
|
1210
|
-
|
1211
|
-
|
1212
|
-
|
1213
|
-
|
1214
|
-
|
1215
|
-
|
1216
|
-
|
1217
|
-
#
|
1218
|
-
|
1219
|
-
|
1220
|
-
|
1221
|
-
|
1222
|
-
|
1223
|
-
|
1224
|
-
|
1225
|
-
|
1226
|
-
|
1227
|
-
|
1228
|
-
|
1229
|
-
|
1230
|
-
|
1231
|
-
|
1232
|
-
|
1233
|
-
|
1234
|
-
|
1235
|
-
|
1236
|
-
|
1237
|
-
#
|
1238
|
-
|
1239
|
-
|
1240
|
-
|
1241
|
-
|
1242
|
-
|
1243
|
-
|
1244
|
-
|
1245
|
-
|
1246
|
-
|
1247
|
-
|
1248
|
-
|
1249
|
-
|
1250
|
-
|
1251
|
-
|
1252
|
-
|
1253
|
-
|
1254
|
-
|
1255
|
-
|
1256
|
-
|
1257
|
-
|
1258
|
-
|
1259
|
-
|
1260
|
-
|
1261
|
-
|
1262
|
-
|
1263
|
-
|
1264
|
-
|
1265
|
-
|
1266
|
-
|
1267
|
-
|
1268
|
-
|
1269
|
-
|
1270
|
-
|
1271
|
-
|
1272
|
-
|
1273
|
-
|
1274
|
-
|
1275
|
-
|
1276
|
-
|
1277
|
-
|
1278
|
-
|
1279
|
-
|
1280
|
-
|
1281
|
-
|
1282
|
-
|
1283
|
-
|
1284
|
-
|
1285
|
-
|
1286
|
-
|
1287
|
-
|
1288
|
-
|
1289
|
-
|
1290
|
-
|
1291
|
-
|
1292
|
-
|
1293
|
-
|
1294
|
-
|
1295
|
-
|
1296
|
-
|
1297
|
-
|
1298
|
-
|
1299
|
-
|
1300
|
-
|
1301
|
-
|
1302
|
-
|
1303
|
-
|
1304
|
-
|
1305
|
-
|
1306
|
-
|
1307
|
-
|
1308
|
-
|
1309
|
-
|
1310
|
-
|
1311
|
-
|
1312
|
-
|
1313
|
-
|
1314
|
-
|
1315
|
-
|
1316
|
-
|
1317
|
-
|
1318
|
-
|
1319
|
-
|
1320
|
-
|
1321
|
-
|
1322
|
-
|
1323
|
-
|
1324
|
-
|
1325
|
-
|
1326
|
-
|
1327
|
-
|
1328
|
-
|
1329
|
-
|
1330
|
-
|
1331
|
-
|
1332
|
-
|
1333
|
-
|
1334
|
-
|
1335
|
-
|
1336
|
-
|
1337
|
-
|
1338
|
-
|
1339
|
-
|
1340
|
-
|
1341
|
-
|
1342
|
-
|
1343
|
-
|
1344
|
-
|
1345
|
-
|
1346
|
-
|
1347
|
-
|
1348
|
-
|
1349
|
-
|
1350
|
-
|
1351
|
-
|
1352
|
-
|
1353
|
-
|
1354
|
-
|
1355
|
-
|
1356
|
-
|
1357
|
-
|
1358
|
-
|
1359
|
-
|
1360
|
-
|
1361
|
-
|
1362
|
-
|
1363
|
-
|
1364
|
-
|
1365
|
-
|
1366
|
-
|
1367
|
-
|
1368
|
-
|
1369
|
-
|
1370
|
-
|
1371
|
-
|
1372
|
-
|
1373
|
-
|
1374
|
-
|
1375
|
-
|
1376
|
-
|
1377
|
-
|
1378
|
-
|
1379
|
-
|
1380
|
-
|
1381
|
-
|
1382
|
-
|
1383
|
-
|
1384
|
-
|
1385
|
-
|
1386
|
-
|
1387
|
-
|
915
|
+
# https://www.dropbox.com/developers/reference/api#metadata
|
916
|
+
def finish(to_path, overwrite=false, parent_rev=nil)
|
917
|
+
response = @client.commit_chunked_upload(to_path, @upload_id, overwrite, parent_rev)
|
918
|
+
Dropbox::parse_response(response)
|
919
|
+
end
|
920
|
+
end
|
921
|
+
|
922
|
+
def commit_chunked_upload(to_path, upload_id, overwrite=false, parent_rev=nil) #:nodoc
|
923
|
+
path = "/commit_chunked_upload/#{@root}#{format_path(to_path)}"
|
924
|
+
params = {'overwrite' => overwrite.to_s,
|
925
|
+
'upload_id' => upload_id,
|
926
|
+
'parent_rev' => parent_rev
|
927
|
+
}
|
928
|
+
headers = nil
|
929
|
+
@session.do_post path, params, headers, :content
|
930
|
+
end
|
931
|
+
|
932
|
+
def partial_chunked_upload(data, upload_id=nil, offset=nil) #:nodoc
|
933
|
+
params = {
|
934
|
+
'upload_id' => upload_id,
|
935
|
+
'offset' => offset,
|
936
|
+
}
|
937
|
+
headers = {'Content-Type' => "application/octet-stream"}
|
938
|
+
@session.do_put '/chunked_upload', params, headers, data, :content
|
939
|
+
end
|
940
|
+
|
941
|
+
# Download a file
|
942
|
+
#
|
943
|
+
# Args:
|
944
|
+
# * +from_path+: The path to the file to be downloaded
|
945
|
+
# * +rev+: A previous revision value of the file to be downloaded
|
946
|
+
#
|
947
|
+
# Returns:
|
948
|
+
# * The file contents.
|
949
|
+
def get_file(from_path, rev=nil)
|
950
|
+
response = get_file_impl(from_path, rev)
|
951
|
+
Dropbox::parse_response(response, raw=true)
|
952
|
+
end
|
953
|
+
|
954
|
+
# Download a file and get its metadata.
|
955
|
+
#
|
956
|
+
# Args:
|
957
|
+
# * +from_path+: The path to the file to be downloaded
|
958
|
+
# * +rev+: A previous revision value of the file to be downloaded
|
959
|
+
#
|
960
|
+
# Returns:
|
961
|
+
# * The file contents.
|
962
|
+
# * The file metadata as a hash.
|
963
|
+
def get_file_and_metadata(from_path, rev=nil)
|
964
|
+
response = get_file_impl(from_path, rev)
|
965
|
+
parsed_response = Dropbox::parse_response(response, raw=true)
|
966
|
+
metadata = parse_metadata(response)
|
967
|
+
return parsed_response, metadata
|
968
|
+
end
|
969
|
+
|
970
|
+
# Download a file (helper method - don't call this directly).
|
971
|
+
#
|
972
|
+
# Args:
|
973
|
+
# * +from_path+: The path to the file to be downloaded
|
974
|
+
# * +rev+: A previous revision value of the file to be downloaded
|
975
|
+
#
|
976
|
+
# Returns:
|
977
|
+
# * The HTTPResponse for the file download request.
|
978
|
+
def get_file_impl(from_path, rev=nil) # :nodoc:
|
979
|
+
path = "/files/#{@root}#{format_path(from_path)}"
|
980
|
+
params = {
|
981
|
+
'rev' => rev,
|
982
|
+
}
|
983
|
+
@session.do_get path, params, :content
|
984
|
+
end
|
985
|
+
private :get_file_impl
|
986
|
+
|
987
|
+
# Parses out file metadata from a raw dropbox HTTP response.
|
988
|
+
#
|
989
|
+
# Args:
|
990
|
+
# * +dropbox_raw_response+: The raw, unparsed HTTPResponse from Dropbox.
|
991
|
+
#
|
992
|
+
# Returns:
|
993
|
+
# * The metadata of the file as a hash.
|
994
|
+
def parse_metadata(dropbox_raw_response) # :nodoc:
|
995
|
+
begin
|
996
|
+
raw_metadata = dropbox_raw_response['x-dropbox-metadata']
|
997
|
+
metadata = JSON.parse(raw_metadata)
|
998
|
+
rescue
|
999
|
+
raise DropboxError.new("Dropbox Server Error: x-dropbox-metadata=#{raw_metadata}",
|
1000
|
+
dropbox_raw_response)
|
1001
|
+
end
|
1002
|
+
metadata
|
1003
|
+
end
|
1004
|
+
private :parse_metadata
|
1005
|
+
|
1006
|
+
# Copy a file or folder to a new location.
|
1007
|
+
#
|
1008
|
+
# Args:
|
1009
|
+
# * +from_path+: The path to the file or folder to be copied.
|
1010
|
+
# * +to_path+: The destination path of the file or folder to be copied.
|
1011
|
+
# This parameter should include the destination filename (e.g.
|
1012
|
+
# from_path: '/test.txt', to_path: '/dir/test.txt'). If there's
|
1013
|
+
# already a file at the to_path, this copy will be renamed to
|
1014
|
+
# be unique.
|
1015
|
+
#
|
1016
|
+
# Returns:
|
1017
|
+
# * A hash with the metadata of the new copy of the file or folder.
|
1018
|
+
# For a detailed description of what this call returns, visit:
|
1019
|
+
# https://www.dropbox.com/developers/reference/api#fileops-copy
|
1020
|
+
def file_copy(from_path, to_path)
|
1021
|
+
params = {
|
1022
|
+
"root" => @root,
|
1023
|
+
"from_path" => format_path(from_path, false),
|
1024
|
+
"to_path" => format_path(to_path, false),
|
1025
|
+
}
|
1026
|
+
response = @session.do_post "/fileops/copy", params
|
1027
|
+
Dropbox::parse_response(response)
|
1028
|
+
end
|
1029
|
+
|
1030
|
+
# Create a folder.
|
1031
|
+
#
|
1032
|
+
# Arguments:
|
1033
|
+
# * +path+: The path of the new folder.
|
1034
|
+
#
|
1035
|
+
# Returns:
|
1036
|
+
# * A hash with the metadata of the newly created folder.
|
1037
|
+
# For a detailed description of what this call returns, visit:
|
1038
|
+
# https://www.dropbox.com/developers/reference/api#fileops-create-folder
|
1039
|
+
def file_create_folder(path)
|
1040
|
+
params = {
|
1041
|
+
"root" => @root,
|
1042
|
+
"path" => format_path(path, false),
|
1043
|
+
}
|
1044
|
+
response = @session.do_post "/fileops/create_folder", params
|
1045
|
+
|
1046
|
+
Dropbox::parse_response(response)
|
1047
|
+
end
|
1048
|
+
|
1049
|
+
# Deletes a file
|
1050
|
+
#
|
1051
|
+
# Arguments:
|
1052
|
+
# * +path+: The path of the file to delete
|
1053
|
+
#
|
1054
|
+
# Returns:
|
1055
|
+
# * A Hash with the metadata of file just deleted.
|
1056
|
+
# For a detailed description of what this call returns, visit:
|
1057
|
+
# https://www.dropbox.com/developers/reference/api#fileops-delete
|
1058
|
+
def file_delete(path)
|
1059
|
+
params = {
|
1060
|
+
"root" => @root,
|
1061
|
+
"path" => format_path(path, false),
|
1062
|
+
}
|
1063
|
+
response = @session.do_post "/fileops/delete", params
|
1064
|
+
Dropbox::parse_response(response)
|
1065
|
+
end
|
1066
|
+
|
1067
|
+
# Moves a file
|
1068
|
+
#
|
1069
|
+
# Arguments:
|
1070
|
+
# * +from_path+: The path of the file to be moved
|
1071
|
+
# * +to_path+: The destination path of the file or folder to be moved
|
1072
|
+
# If the file or folder already exists, it will be renamed to be unique.
|
1073
|
+
#
|
1074
|
+
# Returns:
|
1075
|
+
# * A Hash with the metadata of file or folder just moved.
|
1076
|
+
# For a detailed description of what this call returns, visit:
|
1077
|
+
# https://www.dropbox.com/developers/reference/api#fileops-delete
|
1078
|
+
def file_move(from_path, to_path)
|
1079
|
+
params = {
|
1080
|
+
"root" => @root,
|
1081
|
+
"from_path" => format_path(from_path, false),
|
1082
|
+
"to_path" => format_path(to_path, false),
|
1083
|
+
}
|
1084
|
+
response = @session.do_post "/fileops/move", params
|
1085
|
+
Dropbox::parse_response(response)
|
1086
|
+
end
|
1087
|
+
|
1088
|
+
# Retrives metadata for a file or folder
|
1089
|
+
#
|
1090
|
+
# Arguments:
|
1091
|
+
# * path: The path to the file or folder.
|
1092
|
+
# * list: Whether to list all contained files (only applies when
|
1093
|
+
# path refers to a folder).
|
1094
|
+
# * file_limit: The maximum number of file entries to return within
|
1095
|
+
# a folder. If the number of files in the directory exceeds this
|
1096
|
+
# limit, an exception is raised. The server will return at max
|
1097
|
+
# 25,000 files within a folder.
|
1098
|
+
# * hash: Every directory listing has a hash parameter attached that
|
1099
|
+
# can then be passed back into this function later to save on
|
1100
|
+
# bandwidth. Rather than returning an unchanged folder's contents, if
|
1101
|
+
# the hash matches a DropboxNotModified exception is raised.
|
1102
|
+
# * rev: Optional. The revision of the file to retrieve the metadata for.
|
1103
|
+
# This parameter only applies for files. If omitted, you'll receive
|
1104
|
+
# the most recent revision metadata.
|
1105
|
+
# * include_deleted: Specifies whether to include deleted files in metadata results.
|
1106
|
+
# * include_media_info: Specifies to include media info, such as time_taken for photos
|
1107
|
+
#
|
1108
|
+
# Returns:
|
1109
|
+
# * A Hash object with the metadata of the file or folder (and contained files if
|
1110
|
+
# appropriate). For a detailed description of what this call returns, visit:
|
1111
|
+
# https://www.dropbox.com/developers/reference/api#metadata
|
1112
|
+
def metadata(path, file_limit=25000, list=true, hash=nil, rev=nil, include_deleted=false, include_media_info=false)
|
1113
|
+
params = {
|
1114
|
+
"file_limit" => file_limit.to_s,
|
1115
|
+
"list" => list.to_s,
|
1116
|
+
"include_deleted" => include_deleted.to_s,
|
1117
|
+
"hash" => hash,
|
1118
|
+
"rev" => rev,
|
1119
|
+
"include_media_info" => include_media_info
|
1120
|
+
}
|
1121
|
+
|
1122
|
+
response = @session.do_get "/metadata/#{@root}#{format_path(path)}", params
|
1123
|
+
if response.kind_of? Net::HTTPRedirection
|
1124
|
+
raise DropboxNotModified.new("metadata not modified")
|
1125
|
+
end
|
1126
|
+
Dropbox::parse_response(response)
|
1127
|
+
end
|
1128
|
+
|
1129
|
+
# Search directory for filenames matching query
|
1130
|
+
#
|
1131
|
+
# Arguments:
|
1132
|
+
# * path: The directory to search within
|
1133
|
+
# * query: The query to search on (3 character minimum)
|
1134
|
+
# * file_limit: The maximum number of file entries to return/
|
1135
|
+
# If the number of files exceeds this
|
1136
|
+
# limit, an exception is raised. The server will return at max 1,000
|
1137
|
+
# * include_deleted: Whether to include deleted files in search results
|
1138
|
+
#
|
1139
|
+
# Returns:
|
1140
|
+
# * A Hash object with a list the metadata of the file or folders matching query
|
1141
|
+
# inside path. For a detailed description of what this call returns, visit:
|
1142
|
+
# https://www.dropbox.com/developers/reference/api#search
|
1143
|
+
def search(path, query, file_limit=1000, include_deleted=false)
|
1144
|
+
params = {
|
1145
|
+
'query' => query,
|
1146
|
+
'file_limit' => file_limit.to_s,
|
1147
|
+
'include_deleted' => include_deleted.to_s
|
1148
|
+
}
|
1149
|
+
|
1150
|
+
response = @session.do_get "/search/#{@root}#{format_path(path)}", params
|
1151
|
+
Dropbox::parse_response(response)
|
1152
|
+
end
|
1153
|
+
|
1154
|
+
# Retrive revisions of a file
|
1155
|
+
#
|
1156
|
+
# Arguments:
|
1157
|
+
# * path: The file to fetch revisions for. Note that revisions
|
1158
|
+
# are not available for folders.
|
1159
|
+
# * rev_limit: The maximum number of file entries to return within
|
1160
|
+
# a folder. The server will return at max 1,000 revisions.
|
1161
|
+
#
|
1162
|
+
# Returns:
|
1163
|
+
# * A Hash object with a list of the metadata of the all the revisions of
|
1164
|
+
# all matches files (up to rev_limit entries)
|
1165
|
+
# For a detailed description of what this call returns, visit:
|
1166
|
+
# https://www.dropbox.com/developers/reference/api#revisions
|
1167
|
+
def revisions(path, rev_limit=1000)
|
1168
|
+
params = {
|
1169
|
+
'rev_limit' => rev_limit.to_s
|
1170
|
+
}
|
1171
|
+
|
1172
|
+
response = @session.do_get "/revisions/#{@root}#{format_path(path)}", params
|
1173
|
+
Dropbox::parse_response(response)
|
1174
|
+
end
|
1175
|
+
|
1176
|
+
# Restore a file to a previous revision.
|
1177
|
+
#
|
1178
|
+
# Arguments:
|
1179
|
+
# * path: The file to restore. Note that folders can't be restored.
|
1180
|
+
# * rev: A previous rev value of the file to be restored to.
|
1181
|
+
#
|
1182
|
+
# Returns:
|
1183
|
+
# * A Hash object with a list the metadata of the file or folders restored
|
1184
|
+
# For a detailed description of what this call returns, visit:
|
1185
|
+
# https://www.dropbox.com/developers/reference/api#search
|
1186
|
+
def restore(path, rev)
|
1187
|
+
params = {
|
1188
|
+
'rev' => rev.to_s
|
1189
|
+
}
|
1190
|
+
|
1191
|
+
response = @session.do_post "/restore/#{@root}#{format_path(path)}", params
|
1192
|
+
Dropbox::parse_response(response)
|
1193
|
+
end
|
1194
|
+
|
1195
|
+
# Returns a direct link to a media file
|
1196
|
+
# All of Dropbox's API methods require OAuth, which may cause problems in
|
1197
|
+
# situations where an application expects to be able to hit a URL multiple times
|
1198
|
+
# (for example, a media player seeking around a video file). This method
|
1199
|
+
# creates a time-limited URL that can be accessed without any authentication.
|
1200
|
+
#
|
1201
|
+
# Arguments:
|
1202
|
+
# * path: The file to stream.
|
1203
|
+
#
|
1204
|
+
# Returns:
|
1205
|
+
# * A Hash object that looks like the following:
|
1206
|
+
# {'url': 'https://dl.dropboxusercontent.com/1/view/abcdefghijk/example', 'expires': 'Thu, 16 Sep 2011 01:01:25 +0000'}
|
1207
|
+
def media(path)
|
1208
|
+
response = @session.do_get "/media/#{@root}#{format_path(path)}"
|
1209
|
+
Dropbox::parse_response(response)
|
1210
|
+
end
|
1211
|
+
|
1212
|
+
# Get a URL to share a media file
|
1213
|
+
# Shareable links created on Dropbox are time-limited, but don't require any
|
1214
|
+
# authentication, so they can be given out freely. The time limit should allow
|
1215
|
+
# at least a day of shareability, though users have the ability to disable
|
1216
|
+
# a link from their account if they like.
|
1217
|
+
#
|
1218
|
+
# Arguments:
|
1219
|
+
# * path: The file to share.
|
1220
|
+
# * short_url: When true (default), the url returned will be shortened using the Dropbox url shortener. If false,
|
1221
|
+
# the url will link directly to the file's preview page.
|
1222
|
+
#
|
1223
|
+
# Returns:
|
1224
|
+
# * A Hash object that looks like the following example:
|
1225
|
+
# {'url': 'https://db.tt/c0mFuu1Y', 'expires': 'Tue, 01 Jan 2030 00:00:00 +0000'}
|
1226
|
+
# For a detailed description of what this call returns, visit:
|
1227
|
+
# https://www.dropbox.com/developers/reference/api#shares
|
1228
|
+
def shares(path, short_url=true)
|
1229
|
+
response = @session.do_get "/shares/#{@root}#{format_path(path)}", {"short_url"=>short_url}
|
1230
|
+
Dropbox::parse_response(response)
|
1231
|
+
end
|
1232
|
+
|
1233
|
+
# Download a PDF or HTML preview for a file.
|
1234
|
+
#
|
1235
|
+
# Arguments:
|
1236
|
+
# * path: The path to the file to be previewed.
|
1237
|
+
# * rev: Optional. The revision of the file to retrieve the metadata for.
|
1238
|
+
# If omitted, you'll get the most recent version.
|
1239
|
+
# Returns:
|
1240
|
+
# * The preview data
|
1241
|
+
def preview(path, rev=nil)
|
1242
|
+
path = "/previews/#{@root}#{format_path(path)}"
|
1243
|
+
params = { 'rev' => rev }
|
1244
|
+
response = @session.do_get path, params, :content
|
1245
|
+
Dropbox::parse_response(response, raw=true)
|
1246
|
+
end
|
1247
|
+
|
1248
|
+
# Download a thumbnail for an image.
|
1249
|
+
#
|
1250
|
+
# Arguments:
|
1251
|
+
# * from_path: The path to the file to be thumbnailed.
|
1252
|
+
# * size: A string describing the desired thumbnail size. At this time,
|
1253
|
+
# 'small' (32x32), 'medium' (64x64), 'large' (128x128), 's' (64x64),
|
1254
|
+
# 'm' (128x128), 'l' (640x640), and 'xl' (1024x1024) are officially supported sizes.
|
1255
|
+
# Check https://www.dropbox.com/developers/reference/api#thumbnails
|
1256
|
+
# for more details. [defaults to large]
|
1257
|
+
# Returns:
|
1258
|
+
# * The thumbnail data
|
1259
|
+
def thumbnail(from_path, size='large')
|
1260
|
+
response = thumbnail_impl(from_path, size)
|
1261
|
+
Dropbox::parse_response(response, raw=true)
|
1262
|
+
end
|
1263
|
+
|
1264
|
+
# Download a thumbnail for an image along with the image's metadata.
|
1265
|
+
#
|
1266
|
+
# Arguments:
|
1267
|
+
# * from_path: The path to the file to be thumbnailed.
|
1268
|
+
# * size: A string describing the desired thumbnail size. See thumbnail()
|
1269
|
+
# for details.
|
1270
|
+
# Returns:
|
1271
|
+
# * The thumbnail data
|
1272
|
+
# * The metadata for the image as a hash
|
1273
|
+
def thumbnail_and_metadata(from_path, size='large')
|
1274
|
+
response = thumbnail_impl(from_path, size)
|
1275
|
+
parsed_response = Dropbox::parse_response(response, raw=true)
|
1276
|
+
metadata = parse_metadata(response)
|
1277
|
+
return parsed_response, metadata
|
1278
|
+
end
|
1279
|
+
|
1280
|
+
# A way of letting you keep a local representation of the Dropbox folder
|
1281
|
+
# heirarchy. You can periodically call delta() to get a list of "delta
|
1282
|
+
# entries", which are instructions on how to update your local state to
|
1283
|
+
# match the server's state.
|
1284
|
+
#
|
1285
|
+
# Arguments:
|
1286
|
+
# * +cursor+: On the first call, omit this argument (or pass in +nil+). On
|
1287
|
+
# subsequent calls, pass in the +cursor+ string returned by the previous
|
1288
|
+
# call.
|
1289
|
+
# * +path_prefix+: If provided, results will be limited to files and folders
|
1290
|
+
# whose paths are equal to or under +path_prefix+. The +path_prefix+ is
|
1291
|
+
# fixed for a given cursor. Whatever +path_prefix+ you use on the first
|
1292
|
+
# +delta()+ must also be passed in on subsequent calls that use the returned
|
1293
|
+
# cursor.
|
1294
|
+
#
|
1295
|
+
# Returns: A hash with three fields.
|
1296
|
+
# * +entries+: A list of "delta entries" (described below)
|
1297
|
+
# * +reset+: If +true+, you should reset local state to be an empty folder
|
1298
|
+
# before processing the list of delta entries. This is only +true+ only
|
1299
|
+
# in rare situations.
|
1300
|
+
# * +cursor+: A string that is used to keep track of your current state.
|
1301
|
+
# On the next call to delta(), pass in this value to return entries
|
1302
|
+
# that were recorded since the cursor was returned.
|
1303
|
+
# * +has_more+: If +true+, then there are more entries available; you can
|
1304
|
+
# call delta() again immediately to retrieve those entries. If +false+,
|
1305
|
+
# then wait at least 5 minutes (preferably longer) before checking again.
|
1306
|
+
#
|
1307
|
+
# Delta Entries: Each entry is a 2-item list of one of following forms:
|
1308
|
+
# * [_path_, _metadata_]: Indicates that there is a file/folder at the given
|
1309
|
+
# path. You should add the entry to your local state. (The _metadata_
|
1310
|
+
# value is the same as what would be returned by the #metadata() call.)
|
1311
|
+
# * If the path refers to parent folders that don't yet exist in your
|
1312
|
+
# local state, create those parent folders in your local state. You
|
1313
|
+
# will eventually get entries for those parent folders.
|
1314
|
+
# * If the new entry is a file, replace whatever your local state has at
|
1315
|
+
# _path_ with the new entry.
|
1316
|
+
# * If the new entry is a folder, check what your local state has at
|
1317
|
+
# _path_. If it's a file, replace it with the new entry. If it's a
|
1318
|
+
# folder, apply the new _metadata_ to the folder, but do not modify
|
1319
|
+
# the folder's children.
|
1320
|
+
# * [path, +nil+]: Indicates that there is no file/folder at the _path_ on
|
1321
|
+
# Dropbox. To update your local state to match, delete whatever is at
|
1322
|
+
# _path_, including any children (you will sometimes also get separate
|
1323
|
+
# delta entries for each child, but this is not guaranteed). If your
|
1324
|
+
# local state doesn't have anything at _path_, ignore this entry.
|
1325
|
+
#
|
1326
|
+
# Remember: Dropbox treats file names in a case-insensitive but case-preserving
|
1327
|
+
# way. To facilitate this, the _path_ strings above are lower-cased versions of
|
1328
|
+
# the actual path. The _metadata_ dicts have the original, case-preserved path.
|
1329
|
+
def delta(cursor=nil, path_prefix=nil)
|
1330
|
+
params = {
|
1331
|
+
'cursor' => cursor,
|
1332
|
+
'path_prefix' => path_prefix,
|
1333
|
+
}
|
1334
|
+
|
1335
|
+
response = @session.do_post "/delta", params
|
1336
|
+
Dropbox::parse_response(response)
|
1337
|
+
end
|
1338
|
+
|
1339
|
+
# Calls the long-poll endpoint which waits for changes on an account. In
|
1340
|
+
# conjunction with #delta, this call gives you a low-latency way to monitor
|
1341
|
+
# an account for file changes.
|
1342
|
+
#
|
1343
|
+
# The passed in cursor can only be acquired via a call to #delta
|
1344
|
+
#
|
1345
|
+
# Arguments:
|
1346
|
+
# * +cursor+: A delta cursor as returned from a call to #delta
|
1347
|
+
# * +timeout+: An optional integer indicating a timeout, in seconds. The
|
1348
|
+
# default value is 30 seconds, which is also the minimum allowed value. The
|
1349
|
+
# maximum is 480 seconds.
|
1350
|
+
#
|
1351
|
+
# Returns: A hash with one or two fields.
|
1352
|
+
# * +changes+: A boolean value indicating whether new changes are available.
|
1353
|
+
# * +backoff+: If present, indicates how many seconds your code should wait
|
1354
|
+
# before calling #longpoll_delta again.
|
1355
|
+
def longpoll_delta(cursor, timeout=30)
|
1356
|
+
params = {
|
1357
|
+
'cursor' => cursor,
|
1358
|
+
'timeout' => timeout
|
1359
|
+
}
|
1360
|
+
|
1361
|
+
response = @session.do_get "/longpoll_delta", params, :notify
|
1362
|
+
Dropbox::parse_response(response)
|
1363
|
+
end
|
1364
|
+
|
1365
|
+
# Download a thumbnail (helper method - don't call this directly).
|
1366
|
+
#
|
1367
|
+
# Args:
|
1368
|
+
# * +from_path+: The path to the file to be thumbnailed.
|
1369
|
+
# * +size+: A string describing the desired thumbnail size. See thumbnail()
|
1370
|
+
# for details.
|
1371
|
+
#
|
1372
|
+
# Returns:
|
1373
|
+
# * The HTTPResponse for the thumbnail request.
|
1374
|
+
def thumbnail_impl(from_path, size='large') # :nodoc:
|
1375
|
+
path = "/thumbnails/#{@root}#{format_path(from_path, true)}"
|
1376
|
+
params = {
|
1377
|
+
"size" => size
|
1378
|
+
}
|
1379
|
+
@session.do_get path, params, :content
|
1380
|
+
end
|
1381
|
+
private :thumbnail_impl
|
1382
|
+
|
1383
|
+
|
1384
|
+
# Creates and returns a copy ref for a specific file. The copy ref can be
|
1385
|
+
# used to instantly copy that file to the Dropbox of another account.
|
1386
|
+
#
|
1387
|
+
# Args:
|
1388
|
+
# * +path+: The path to the file for a copy ref to be created on.
|
1389
|
+
#
|
1390
|
+
# Returns:
|
1391
|
+
# * A Hash object that looks like the following example:
|
1392
|
+
# {"expires"=>"Fri, 31 Jan 2042 21:01:05 +0000", "copy_ref"=>"z1X6ATl6aWtzOGq0c3g5Ng"}
|
1393
|
+
def create_copy_ref(path)
|
1394
|
+
path = "/copy_ref/#{@root}#{format_path(path)}"
|
1395
|
+
response = @session.do_get path
|
1396
|
+
Dropbox::parse_response(response)
|
1397
|
+
end
|
1398
|
+
|
1399
|
+
# Adds the file referenced by the copy ref to the specified path
|
1400
|
+
#
|
1401
|
+
# Args:
|
1402
|
+
# * +copy_ref+: A copy ref string that was returned from a create_copy_ref call.
|
1403
|
+
# The copy_ref can be created from any other Dropbox account, or from the same account.
|
1404
|
+
# * +to_path+: The path to where the file will be created.
|
1405
|
+
#
|
1406
|
+
# Returns:
|
1407
|
+
# * A hash with the metadata of the new file.
|
1408
|
+
def add_copy_ref(to_path, copy_ref)
|
1409
|
+
params = {'from_copy_ref' => copy_ref,
|
1410
|
+
'to_path' => "#{to_path}",
|
1411
|
+
'root' => @root}
|
1412
|
+
|
1413
|
+
response = @session.do_post "/fileops/copy", params
|
1414
|
+
Dropbox::parse_response(response)
|
1415
|
+
end
|
1416
|
+
|
1417
|
+
#From the oauth spec plus "/". Slash should not be ecsaped
|
1418
|
+
RESERVED_CHARACTERS = /[^a-zA-Z0-9\-\.\_\~\/]/ # :nodoc:
|
1419
|
+
|
1420
|
+
def format_path(path, escape=true) # :nodoc:
|
1421
|
+
path = path.gsub(/\/+/,"/")
|
1422
|
+
# replace multiple slashes with a single one
|
1423
|
+
|
1424
|
+
path = path.gsub(/^\/?/,"/")
|
1425
|
+
# ensure the path starts with a slash
|
1426
|
+
|
1427
|
+
path.gsub(/\/?$/,"")
|
1428
|
+
# ensure the path doesn't end with a slash
|
1429
|
+
|
1430
|
+
return URI.escape(path, RESERVED_CHARACTERS) if escape
|
1431
|
+
path
|
1432
|
+
end
|
1388
1433
|
|
1389
1434
|
end
|