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.
@@ -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:4567/dropbox-auth-finish as a Redirect URI for your Dropbox app.
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:4567/
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
- return DropboxOAuth2Flow.new(APP_KEY, APP_SECRET, url('/dropbox-auth-finish'),
33
- session, :dropbox_auth_csrf_token)
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
- authorize_url = get_web_auth().start()
37
+ authorize_url = get_web_auth().start()
38
38
 
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
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
- 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('/')
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
- session.delete(:access_token)
71
- nil
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
- if session[:access_token]
77
- return DropboxClient.new(session[:access_token])
78
- end
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
- # 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}"
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
- render_file(client, entry)
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
- # 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
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
- # Just dump out metadata hash
135
- html_page "File: #{entry['path']}", "<pre>#{h entry.pretty_inspect}</pre>"
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
- # 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>"
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
- "<html>" +
173
- "<head><title>#{h title}</title></head>" +
174
- "<body><h1>#{h title}</h1>#{body}</body>" +
175
- "</html>"
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
- include Rack::Utils
187
- alias_method :h, :escape_html
186
+ include Rack::Utils
187
+ alias_method :h, :escape_html
188
188
  end
189
189
 
190
190
  if APP_KEY == '' or APP_SECRET == ''
191
- puts "You must set APP_KEY and APP_SECRET at the top of \"#{__FILE__}\"!"
192
- exit 1
191
+ puts "You must set APP_KEY and APP_SECRET at the top of \"#{__FILE__}\"!"
192
+ exit 1
193
193
  end
@@ -8,208 +8,222 @@ require 'securerandom'
8
8
  require 'pp'
9
9
 
10
10
  module Dropbox # :nodoc:
11
- API_SERVER = "api.dropbox.com"
12
- API_CONTENT_SERVER = "api-content.dropbox.com"
13
- WEB_SERVER = "www.dropbox.com"
14
-
15
- API_VERSION = 1
16
- SDK_VERSION = "1.6.4"
17
-
18
- TRUSTED_CERT_FILE = File.join(File.dirname(__FILE__), 'trusted-certs.crt')
19
-
20
- def self.clean_params(params)
21
- r = {}
22
- params.each do |k,v|
23
- r[k] = v.to_s if not v.nil?
24
- end
25
- r
26
- end
27
-
28
- def self.make_query_string(params)
29
- clean_params(params).collect {|k,v|
30
- CGI.escape(k) + "=" + CGI.escape(v)
31
- }.join("&")
32
- end
33
-
34
- def self.verify_ssl_certificate(preverify_ok, ssl_context)
35
- if preverify_ok != true || ssl_context.error != 0
36
- err_msg = "SSL Verification failed -- Preverify: #{preverify_ok}, Error: #{ssl_context.error_string} (#{ssl_context.error})"
37
- raise OpenSSL::SSL::SSLError.new(err_msg)
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
- 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
-
50
- if RUBY_VERSION >= '1.9'
51
- # SSL protocol and ciphersuite settings are supported strating with version 1.9
52
- http.ssl_version = 'TLSv1'
53
- http.ciphers = 'ECDHE-RSA-AES256-GCM-SHA384:'\
54
- 'ECDHE-RSA-AES256-SHA384:'\
55
- 'ECDHE-RSA-AES256-SHA:'\
56
- 'ECDHE-RSA-AES128-GCM-SHA256:'\
57
- 'ECDHE-RSA-AES128-SHA256:'\
58
- 'ECDHE-RSA-AES128-SHA:'\
59
- 'ECDHE-RSA-RC4-SHA:'\
60
- 'DHE-RSA-AES256-GCM-SHA384:'\
61
- 'DHE-RSA-AES256-SHA256:'\
62
- 'DHE-RSA-AES256-SHA:'\
63
- 'DHE-RSA-AES128-GCM-SHA256:'\
64
- 'DHE-RSA-AES128-SHA256:'\
65
- 'DHE-RSA-AES128-SHA:'\
66
- 'AES256-GCM-SHA384:'\
67
- 'AES256-SHA256:'\
68
- 'AES256-SHA:'\
69
- 'AES128-GCM-SHA256:'\
70
- 'AES128-SHA256:'\
71
- 'AES128-SHA'
72
- end
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
- # Parse response. You probably shouldn't be calling this directly. This takes responses from the server
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
- return response.body if raw
117
-
118
- begin
119
- return JSON.parse(response.body)
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
- # A string comparison function that is resistant to timing attacks. If you're comparing a
126
- # string you got from the outside world with a string that is supposed to be a secret, use
127
- # this function to check equality.
128
- def self.safe_string_equals(a, b)
129
- if a.length != b.length
130
- false
131
- else
132
- a.chars.zip(b.chars).map {|ac,bc| ac == bc}.all?
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
- attr_writer :locale
140
-
141
- def initialize(locale)
142
- @locale = locale
143
- end
144
-
145
- private
146
-
147
- def build_url(path, content_server)
148
- port = 443
149
- host = content_server ? Dropbox::API_CONTENT_SERVER : Dropbox::API_SERVER
150
- full_path = "/#{Dropbox::API_VERSION}#{path}"
151
- return URI::HTTPS.build({:host => host, :path => full_path})
152
- end
153
-
154
- def build_url_with_params(path, params, content_server) # :nodoc:
155
- target = build_url(path, content_server)
156
- params['locale'] = @locale
157
- target.query = Dropbox::make_query_string(params)
158
- return target
159
- end
160
-
161
- protected
162
-
163
- def do_http(uri, request) # :nodoc:
164
- sign_request(request)
165
- Dropbox::do_http(uri, request)
166
- end
167
-
168
- public
169
-
170
- def do_get(path, params=nil, headers=nil, content_server=false) # :nodoc:
171
- params ||= {}
172
- assert_authorized
173
- uri = build_url_with_params(path, params, content_server)
174
- do_http(uri, Net::HTTP::Get.new(uri.request_uri))
175
- end
176
-
177
- def do_http_with_body(uri, request, body)
178
- if body != nil
179
- if body.is_a?(Hash)
180
- request.set_form_data(Dropbox::clean_params(body))
181
- elsif body.respond_to?(:read)
182
- if body.respond_to?(:length)
183
- request["Content-Length"] = body.length.to_s
184
- elsif body.respond_to?(:stat) && body.stat.respond_to?(:size)
185
- request["Content-Length"] = body.stat.size.to_s
186
- else
187
- raise ArgumentError, "Don't know how to handle 'body' (responds to 'read' but not to 'length' or 'stat.size')."
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
- do_http(uri, request)
197
- end
198
-
199
- def do_post(path, params=nil, headers=nil, content_server=false) # :nodoc:
200
- params ||= {}
201
- assert_authorized
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
- # * consumer_key - Your Dropbox application's "app key".
221
- # * consumer_secret - Your Dropbox application's "app secret".
222
- def initialize(consumer_key, consumer_secret, locale=nil)
223
- super(locale)
224
- @consumer_key = consumer_key
225
- @consumer_secret = consumer_secret
226
- @request_token = nil
227
- @access_token = nil
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
- private
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
- def build_auth_header(token) # :nodoc:
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
- def do_get_with_token(url, token, headers=nil) # :nodoc:
246
- uri = URI.parse(url)
247
- request = Net::HTTP::Get.new(uri.request_uri)
248
- request.add_field('Authorization', build_auth_header(token))
249
- Dropbox::do_http(uri, request)
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
- protected
253
-
254
- def sign_request(request) # :nodoc:
255
- request.add_field('Authorization', build_auth_header(@access_token))
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
- public
288
+ OAuthToken.new(parts["oauth_token"][0], parts["oauth_token_secret"][0])
289
+ end
259
290
 
260
- def get_token(url_end, input_token, error_message_prefix) #: nodoc:
261
- response = do_get_with_token("https://#{Dropbox::API_SERVER}:443/#{Dropbox::API_VERSION}/oauth#{url_end}", input_token)
262
- if not response.kind_of?(Net::HTTPSuccess) # it must be a 200
263
- raise DropboxAuthError.new("#{error_message_prefix} Server returned #{response.code}: #{response.message}.", response)
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
- if !parts.has_key? "oauth_token" and parts["oauth_token"].length != 1
268
- raise DropboxAuthError.new("Invalid response from #{url_end}: missing \"oauth_token\" parameter: #{response.body}", response)
269
- end
270
- if !parts.has_key? "oauth_token_secret" and parts["oauth_token_secret"].length != 1
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
- OAuthToken.new(parts["oauth_token"][0], parts["oauth_token_secret"][0])
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
- # This returns a request token. Requests one from the dropbox server using the provided application key and secret if nessecary.
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
- # This returns a URL that your user must visit to grant
283
- # permissions to this application.
284
- def get_authorize_url(callback=nil)
285
- get_request_token()
309
+ "https://#{Dropbox::WEB_SERVER}#{url}"
310
+ end
286
311
 
287
- url = "/#{Dropbox::API_VERSION}/oauth/authorize?oauth_token=#{URI.escape(@request_token.key)}"
288
- if callback
289
- url += "&oauth_callback=#{URI.escape(callback)}"
290
- end
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
- "https://#{Dropbox::WEB_SERVER}#{url}"
296
- end
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
- # Clears the access_token
299
- def clear_access_token
300
- @access_token = nil
301
- end
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
- # Returns the request token, or nil if one hasn't been acquired yet.
304
- def request_token
305
- @request_token
306
- end
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
- # Returns the access token, or nil if one hasn't been acquired yet.
309
- def access_token
310
- @access_token
311
- end
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
- # Given a saved request token and secret, set this location's token and secret
314
- # * token - this is the request token
315
- # * secret - this is the request token secret
316
- def set_request_token(key, secret)
317
- @request_token = OAuthToken.new(key, secret)
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
- # Given a saved access token and secret, you set this Session to use that token and secret
321
- # * token - this is the access token
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
- # Returns the access token. If this DropboxSession doesn't yet have an access_token, it requests one
328
- # using the request_token generate from your app's token and secret. This request will fail unless
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
- if @request_token.nil?
334
- raise RuntimeError.new("No request token. You must set this or get an authorize url first.")
335
- end
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
- # If we have an access token, then do nothing. If not, throw a RuntimeError.
341
- def assert_authorized
342
- unless authorized?
343
- raise RuntimeError.new('Session does not yet have a request token')
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
- # Returns true if this Session has been authorized and has an access_token.
348
- def authorized?
349
- !!@access_token
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
- # serialize the DropboxSession.
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
- get_request_token
379
+ toreturn.push @request_token.secret, @request_token.key
380
+ toreturn.push @consumer_secret, @consumer_key
364
381
 
365
- toreturn.push @request_token.secret, @request_token.key
366
- toreturn.push @consumer_secret, @consumer_key
382
+ toreturn.to_yaml
383
+ end
367
384
 
368
- toreturn.to_yaml
369
- end
370
-
371
- # Takes a serialized DropboxSession YAML String and returns a new DropboxSession object
372
- def self.deserialize(ser)
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
- if ser.length > 0
378
- session.set_access_token(ser.pop, ser.pop)
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
- def initialize(oauth2_access_token, locale=nil)
388
- super(locale)
389
- if not oauth2_access_token.is_a?(String)
390
- raise "bad type for oauth2_access_token (expecting String)"
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
- def assert_authorized
396
- true
397
- end
409
+ def assert_authorized
410
+ true
411
+ end
398
412
 
399
- protected
413
+ protected
400
414
 
401
- def sign_request(request) # :nodoc:
402
- request.add_field('Authorization', 'Bearer ' + @access_token)
403
- end
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
- def initialize(consumer_key, consumer_secret, locale=nil)
409
- if not consumer_key.is_a?(String)
410
- raise ArgumentError, "consumer_key must be a String, got #{consumer_key.inspect}"
411
- end
412
- if not consumer_secret.is_a?(String)
413
- raise ArgumentError, "consumer_secret must be a String, got #{consumer_secret.inspect}"
414
- end
415
- if not (locale.nil? or locale.is_a?(String))
416
- raise ArgumentError, "locale must be a String or nil, got #{locale.inspect}"
417
- end
418
- @consumer_key = consumer_key
419
- @consumer_secret = consumer_secret
420
- @locale = locale
421
- end
422
-
423
- def _get_authorize_url(redirect_uri, state)
424
- params = {
425
- "client_id" => @consumer_key,
426
- "response_type" => "code",
427
- "redirect_uri" => redirect_uri,
428
- "state" => state,
429
- "locale" => @locale,
430
- }
431
-
432
- host = Dropbox::WEB_SERVER
433
- path = "/#{Dropbox::API_VERSION}/oauth2/authorize"
434
-
435
- target = URI::Generic.new("https", nil, host, nil, nil, path, nil, nil, nil)
436
- target.query = Dropbox::make_query_string(params)
437
-
438
- target.to_s
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
- # Finish the OAuth 2 authorization process. If you used a redirect_uri, pass that in.
442
- # Will return an access token string that you can use with DropboxClient.
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
- # * consumer_key: Your Dropbox API app's "app key"
486
- # * consumer_secret: Your Dropbox API app's "app secret"
487
- # * locale: The locale of the user currently using your app.
488
- def initialize(consumer_key, consumer_secret, locale=nil)
489
- super(consumer_key, consumer_secret, locale)
490
- end
491
-
492
- # Returns a authorization_url, which is a page on Dropbox's website. Have the user
493
- # visit this URL and approve your app.
494
- def start()
495
- _get_authorize_url(nil, nil)
496
- end
497
-
498
- # If the user approves your app, they will be presented with an "authorization code".
499
- # Have the user copy/paste that authorization code into your app and then call this
500
- # method to get an access token.
501
- #
502
- # Returns a two-entry list (access_token, user_id)
503
- # * access_token is an access token string that can be passed to DropboxClient.
504
- # * user_id is the Dropbox user ID of the user that just approved your app.
505
- def finish(code)
506
- _finish(code, nil)
507
- end
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
- # * consumer_key: Your Dropbox API app's "app key"
514
- # * consumer_secret: Your Dropbox API app's "app secret"
515
- # * redirect_uri: The URI that the Dropbox server will redirect the user to after the user
516
- # finishes authorizing your app. This URI must be HTTPs-based and pre-registered with
517
- # the Dropbox servers, though localhost URIs are allowed without pre-registration and can
518
- # be either HTTP or HTTPS.
519
- # * session: A hash that represents the current web app session (will be used to save the CSRF
520
- # token)
521
- # * csrf_token_key: The key to use when storing the CSRF token in the session (for example,
522
- # :dropbox_auth_csrf_token)
523
- # * locale: The locale of the user currently using your app (ex: "en" or "en_US").
524
- def initialize(consumer_key, consumer_secret, redirect_uri, session, csrf_token_session_key, locale=nil)
525
- super(consumer_key, consumer_secret, locale)
526
- if not redirect_uri.is_a?(String)
527
- raise ArgumentError, "redirect_uri must be a String, got #{consumer_secret.inspect}"
528
- end
529
- @redirect_uri = redirect_uri
530
- @session = session
531
- @csrf_token_session_key = csrf_token_session_key
532
- end
533
-
534
- # Starts the OAuth 2 authorizaton process, which involves redirecting the user to
535
- # the returned "authorization URL" (a URL on the Dropbox website). When the user then
536
- # either approves or denies your app access, Dropbox will redirect them to the
537
- # redirect_uri you provided to the constructor, at which point you should call finish()
538
- # to complete the process.
539
- #
540
- # This function will also save a CSRF token to the session and csrf_token_session_key
541
- # you provided to the constructor. This CSRF token will be checked on finish() to prevent
542
- # request forgery.
543
- #
544
- # * url_state: Any data you would like to keep in the URL through the authorization
545
- # process. This exact value will be returned to you by finish().
546
- #
547
- # Returns the URL to redirect the user to.
548
- def start(url_state=nil)
549
- unless url_state.nil? or url_state.is_a?(String)
550
- raise ArgumentError, "url_state must be a String"
551
- end
552
-
553
- csrf_token = SecureRandom.base64(16)
554
- state = csrf_token
555
- unless url_state.nil?
556
- state += "|" + url_state
557
- end
558
- @session[@csrf_token_session_key] = csrf_token
559
-
560
- return _get_authorize_url(@redirect_uri, state)
561
- end
562
-
563
- # Call this after the user has visited the authorize URL (see: start()), approved your app,
564
- # and was redirected to your redirect URI.
565
- #
566
- # * query_params: The query params on the GET request to your redirect URI.
567
- #
568
- # Returns a tuple of (access_token, user_id, url_state). access_token can be used to
569
- # construct a DropboxClient. user_id is the Dropbox user ID of the user that jsut approved
570
- # your app. url_state is the value you originally passed in to start().
571
- #
572
- # Can throw BadRequestError, BadStateError, CsrfError, NotApprovedError,
573
- # ProviderError, and the standard DropboxError.
574
- def finish(query_params)
575
- csrf_token_from_session = @session[@csrf_token_session_key]
576
-
577
- # Check well-formedness of request.
578
-
579
- state = query_params['state']
580
- if state.nil?
581
- raise BadRequestError.new("Missing query parameter 'state'.")
582
- end
583
-
584
- error = query_params['error']
585
- error_description = query_params['error_description']
586
- code = query_params['code']
587
-
588
- if not error.nil? and not code.nil?
589
- raise BadRequestError.new("Query parameters 'code' and 'error' are both set;" +
590
- " only one must be set.")
591
- end
592
- if error.nil? and code.nil?
593
- raise BadRequestError.new("Neither query parameter 'code' or 'error' is set.")
594
- end
595
-
596
- # Check CSRF token
597
-
598
- if csrf_token_from_session.nil?
599
- raise BadStateError.new("Missing CSRF token in session.");
600
- end
601
- unless csrf_token_from_session.length > 20
602
- raise RuntimeError.new("CSRF token unexpectedly short: #{csrf_token_from_session.inspect}")
603
- end
604
-
605
- split_pos = state.index('|')
606
- if split_pos.nil?
607
- given_csrf_token = state
608
- url_state = nil
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
- given_csrf_token, url_state = state.split('|', 2)
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
- @session.delete(@csrf_token_session_key)
617
-
618
- # Check for error identifier
619
-
620
- if not error.nil?
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
- # If everything went ok, make the network call to get an access token.
652
+ # If everything went ok, make the network call to get an access token.
639
653
 
640
- access_token, user_id = _finish(code, @redirect_uri)
641
- return access_token, user_id, url_state
642
- end
654
+ access_token, user_id = _finish(code, @redirect_uri)
655
+ return access_token, user_id, url_state
656
+ end
643
657
 
644
- # Thrown if the redirect URL was missing parameters or if the given parameters were not valid.
645
- #
646
- # The recommended action is to show an HTTP 400 error page.
647
- class BadRequestError < Exception; end
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
- # Thrown if all the parameters are correct, but there's no CSRF token in the session. This
650
- # probably means that the session expired.
651
- #
652
- # The recommended action is to redirect the user's browser to try the approval process again.
653
- class BadStateError < Exception; end
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
- # Thrown if the given 'state' parameter doesn't contain the CSRF token from the user's session.
656
- # This is blocked to prevent CSRF attacks.
657
- #
658
- # The recommended action is to respond with an HTTP 403 error page.
659
- class CsrfError < Exception; end
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
- # The user chose not to approve your app.
662
- class NotApprovedError < Exception; end
675
+ # The user chose not to approve your app.
676
+ class NotApprovedError < Exception; end
663
677
 
664
- # Dropbox redirected to your redirect URI with some unexpected error identifier and error
665
- # message.
666
- class ProviderError < Exception; end
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
- def initialize(key, secret)
673
- @key = key
674
- @secret = secret
675
- end
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
- attr_accessor :http_response, :error, :user_error
690
- def initialize(error, http_response=nil, user_error=nil)
691
- @error = error
692
- @http_response = http_response
693
- @user_error = user_error
694
- end
695
-
696
- def to_s
697
- return "#{user_error} (#{error})" if user_error
698
- "#{error}"
699
- end
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
- # * +oauth2_access_token+: Obtained via DropboxOAuth2Flow or DropboxOAuth2FlowNoRedirect.
721
- # * +locale+: The user's current locale (used to localize error messages).
722
- def initialize(oauth2_access_token, root="auto", locale=nil)
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
- @root = root.to_s # If they passed in a symbol, make it a string
736
-
737
- if not ["dropbox","app_folder","auto"].include?(@root)
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
- # Returns some information about the current user's Dropbox account (the "current user"
748
- # is the user associated with the access token you're using).
749
- #
750
- # For a detailed description of what this call returns, visit:
751
- # https://www.dropbox.com/developers/reference/api#account-info
752
- def account_info()
753
- response = @session.do_get "/account/info"
754
- Dropbox::parse_response(response)
755
- end
756
-
757
- # Disables the access token that this +DropboxClient+ is using. If this call
758
- # succeeds, further API calls using this object will fail.
759
- def disable_access_token
760
- @session.do_post "/disable_access_token"
761
- nil
762
- end
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
- # If this +DropboxClient+ was created with an OAuth 1 access token, this method
765
- # can be used to create an equivalent OAuth 2 access token. This can be used to
766
- # upgrade your app's existing access tokens from OAuth 1 to OAuth 2.
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
- response = @session.do_post "/oauth2/token_from_oauth1"
773
- Dropbox::parse_response(response)['access_token']
887
+ @upload_id = resp['upload_id'] if resp['upload_id']
888
+ end
774
889
  end
775
890
 
776
- # Uploads a file to a server. This uses the HTTP PUT upload method for simplicity
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
- # * +parent_rev+: The rev field from the 'parent' of this upload. [optional]
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 or folder just moved.
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#fileops-delete
1076
- def file_move(from_path, to_path)
1077
- params = {
1078
- "root" => @root,
1079
- "from_path" => format_path(from_path, false),
1080
- "to_path" => format_path(to_path, false),
1081
- }
1082
- response = @session.do_post "/fileops/move", params
1083
- Dropbox::parse_response(response)
1084
- end
1085
-
1086
- # Retrives metadata for a file or folder
1087
- #
1088
- # Arguments:
1089
- # * path: The path to the file or folder.
1090
- # * list: Whether to list all contained files (only applies when
1091
- # path refers to a folder).
1092
- # * file_limit: The maximum number of file entries to return within
1093
- # a folder. If the number of files in the directory exceeds this
1094
- # limit, an exception is raised. The server will return at max
1095
- # 25,000 files within a folder.
1096
- # * hash: Every directory listing has a hash parameter attached that
1097
- # can then be passed back into this function later to save on
1098
- # bandwidth. Rather than returning an unchanged folder's contents, if
1099
- # the hash matches a DropboxNotModified exception is raised.
1100
- # * rev: Optional. The revision of the file to retrieve the metadata for.
1101
- # This parameter only applies for files. If omitted, you'll receive
1102
- # the most recent revision metadata.
1103
- # * include_deleted: Specifies whether to include deleted files in metadata results.
1104
- #
1105
- # Returns:
1106
- # * A Hash object with the metadata of the file or folder (and contained files if
1107
- # appropriate). For a detailed description of what this call returns, visit:
1108
- # https://www.dropbox.com/developers/reference/api#metadata
1109
- def metadata(path, file_limit=25000, list=true, hash=nil, rev=nil, include_deleted=false)
1110
- params = {
1111
- "file_limit" => file_limit.to_s,
1112
- "list" => list.to_s,
1113
- "include_deleted" => include_deleted.to_s,
1114
- "hash" => hash,
1115
- "rev" => rev,
1116
- }
1117
-
1118
- response = @session.do_get "/metadata/#{@root}#{format_path(path)}", params
1119
- if response.kind_of? Net::HTTPRedirection
1120
- raise DropboxNotModified.new("metadata not modified")
1121
- end
1122
- Dropbox::parse_response(response)
1123
- end
1124
-
1125
- # Search directory for filenames matching query
1126
- #
1127
- # Arguments:
1128
- # * path: The directory to search within
1129
- # * query: The query to search on (3 character minimum)
1130
- # * file_limit: The maximum number of file entries to return/
1131
- # If the number of files exceeds this
1132
- # limit, an exception is raised. The server will return at max 1,000
1133
- # * include_deleted: Whether to include deleted files in search results
1134
- #
1135
- # Returns:
1136
- # * A Hash object with a list the metadata of the file or folders matching query
1137
- # inside path. For a detailed description of what this call returns, visit:
1138
- # https://www.dropbox.com/developers/reference/api#search
1139
- def search(path, query, file_limit=1000, include_deleted=false)
1140
- params = {
1141
- 'query' => query,
1142
- 'file_limit' => file_limit.to_s,
1143
- 'include_deleted' => include_deleted.to_s
1144
- }
1145
-
1146
- response = @session.do_get "/search/#{@root}#{format_path(path)}", params
1147
- Dropbox::parse_response(response)
1148
- end
1149
-
1150
- # Retrive revisions of a file
1151
- #
1152
- # Arguments:
1153
- # * path: The file to fetch revisions for. Note that revisions
1154
- # are not available for folders.
1155
- # * rev_limit: The maximum number of file entries to return within
1156
- # a folder. The server will return at max 1,000 revisions.
1157
- #
1158
- # Returns:
1159
- # * A Hash object with a list of the metadata of the all the revisions of
1160
- # all matches files (up to rev_limit entries)
1161
- # For a detailed description of what this call returns, visit:
1162
- # https://www.dropbox.com/developers/reference/api#revisions
1163
- def revisions(path, rev_limit=1000)
1164
- params = {
1165
- 'rev_limit' => rev_limit.to_s
1166
- }
1167
-
1168
- response = @session.do_get "/revisions/#{@root}#{format_path(path)}", params
1169
- Dropbox::parse_response(response)
1170
- end
1171
-
1172
- # Restore a file to a previous revision.
1173
- #
1174
- # Arguments:
1175
- # * path: The file to restore. Note that folders can't be restored.
1176
- # * rev: A previous rev value of the file to be restored to.
1177
- #
1178
- # Returns:
1179
- # * A Hash object with a list the metadata of the file or folders restored
1180
- # For a detailed description of what this call returns, visit:
1181
- # https://www.dropbox.com/developers/reference/api#search
1182
- def restore(path, rev)
1183
- params = {
1184
- 'rev' => rev.to_s
1185
- }
1186
-
1187
- response = @session.do_post "/restore/#{@root}#{format_path(path)}", params
1188
- Dropbox::parse_response(response)
1189
- end
1190
-
1191
- # Returns a direct link to a media file
1192
- # All of Dropbox's API methods require OAuth, which may cause problems in
1193
- # situations where an application expects to be able to hit a URL multiple times
1194
- # (for example, a media player seeking around a video file). This method
1195
- # creates a time-limited URL that can be accessed without any authentication.
1196
- #
1197
- # Arguments:
1198
- # * path: The file to stream.
1199
- #
1200
- # Returns:
1201
- # * A Hash object that looks like the following:
1202
- # {'url': 'https://dl.dropboxusercontent.com/1/view/abcdefghijk/example', 'expires': 'Thu, 16 Sep 2011 01:01:25 +0000'}
1203
- def media(path)
1204
- response = @session.do_get "/media/#{@root}#{format_path(path)}"
1205
- Dropbox::parse_response(response)
1206
- end
1207
-
1208
- # Get a URL to share a media file
1209
- # Shareable links created on Dropbox are time-limited, but don't require any
1210
- # authentication, so they can be given out freely. The time limit should allow
1211
- # at least a day of shareability, though users have the ability to disable
1212
- # a link from their account if they like.
1213
- #
1214
- # Arguments:
1215
- # * path: The file to share.
1216
- #
1217
- # Returns:
1218
- # * A Hash object that looks like the following example:
1219
- # {'url': 'https://db.tt/c0mFuu1Y', 'expires': 'Tue, 01 Jan 2030 00:00:00 +0000'}
1220
- # For a detailed description of what this call returns, visit:
1221
- # https://www.dropbox.com/developers/reference/api#shares
1222
- def shares(path)
1223
- response = @session.do_get "/shares/#{@root}#{format_path(path)}"
1224
- Dropbox::parse_response(response)
1225
- end
1226
-
1227
- # Download a thumbnail for an image.
1228
- #
1229
- # Arguments:
1230
- # * from_path: The path to the file to be thumbnailed.
1231
- # * size: A string describing the desired thumbnail size. At this time,
1232
- # 'small' (32x32), 'medium' (64x64), 'large' (128x128), 's' (64x64),
1233
- # 'm' (128x128), 'l' (640x640), and 'xl' (1024x1024) are officially supported sizes.
1234
- # Check https://www.dropbox.com/developers/reference/api#thumbnails
1235
- # for more details. [defaults to large]
1236
- # Returns:
1237
- # * The thumbnail data
1238
- def thumbnail(from_path, size='large')
1239
- response = thumbnail_impl(from_path, size)
1240
- Dropbox::parse_response(response, raw=true)
1241
- end
1242
-
1243
- # Download a thumbnail for an image along with the image's metadata.
1244
- #
1245
- # Arguments:
1246
- # * from_path: The path to the file to be thumbnailed.
1247
- # * size: A string describing the desired thumbnail size. See thumbnail()
1248
- # for details.
1249
- # Returns:
1250
- # * The thumbnail data
1251
- # * The metadata for the image as a hash
1252
- def thumbnail_and_metadata(from_path, size='large')
1253
- response = thumbnail_impl(from_path, size)
1254
- parsed_response = Dropbox::parse_response(response, raw=true)
1255
- metadata = parse_metadata(response)
1256
- return parsed_response, metadata
1257
- end
1258
-
1259
- # A way of letting you keep a local representation of the Dropbox folder
1260
- # heirarchy. You can periodically call delta() to get a list of "delta
1261
- # entries", which are instructions on how to update your local state to
1262
- # match the server's state.
1263
- #
1264
- # Arguments:
1265
- # * +cursor+: On the first call, omit this argument (or pass in +nil+). On
1266
- # subsequent calls, pass in the +cursor+ string returned by the previous
1267
- # call.
1268
- # * +path_prefix+: If provided, results will be limited to files and folders
1269
- # whose paths are equal to or under +path_prefix+. The +path_prefix+ is
1270
- # fixed for a given cursor. Whatever +path_prefix+ you use on the first
1271
- # +delta()+ must also be passed in on subsequent calls that use the returned
1272
- # cursor.
1273
- #
1274
- # Returns: A hash with three fields.
1275
- # * +entries+: A list of "delta entries" (described below)
1276
- # * +reset+: If +true+, you should reset local state to be an empty folder
1277
- # before processing the list of delta entries. This is only +true+ only
1278
- # in rare situations.
1279
- # * +cursor+: A string that is used to keep track of your current state.
1280
- # On the next call to delta(), pass in this value to return entries
1281
- # that were recorded since the cursor was returned.
1282
- # * +has_more+: If +true+, then there are more entries available; you can
1283
- # call delta() again immediately to retrieve those entries. If +false+,
1284
- # then wait at least 5 minutes (preferably longer) before checking again.
1285
- #
1286
- # Delta Entries: Each entry is a 2-item list of one of following forms:
1287
- # * [_path_, _metadata_]: Indicates that there is a file/folder at the given
1288
- # path. You should add the entry to your local state. (The _metadata_
1289
- # value is the same as what would be returned by the #metadata() call.)
1290
- # * If the path refers to parent folders that don't yet exist in your
1291
- # local state, create those parent folders in your local state. You
1292
- # will eventually get entries for those parent folders.
1293
- # * If the new entry is a file, replace whatever your local state has at
1294
- # _path_ with the new entry.
1295
- # * If the new entry is a folder, check what your local state has at
1296
- # _path_. If it's a file, replace it with the new entry. If it's a
1297
- # folder, apply the new _metadata_ to the folder, but do not modify
1298
- # the folder's children.
1299
- # * [path, +nil+]: Indicates that there is no file/folder at the _path_ on
1300
- # Dropbox. To update your local state to match, delete whatever is at
1301
- # _path_, including any children (you will sometimes also get separate
1302
- # delta entries for each child, but this is not guaranteed). If your
1303
- # local state doesn't have anything at _path_, ignore this entry.
1304
- #
1305
- # Remember: Dropbox treats file names in a case-insensitive but case-preserving
1306
- # way. To facilitate this, the _path_ strings above are lower-cased versions of
1307
- # the actual path. The _metadata_ dicts have the original, case-preserved path.
1308
- def delta(cursor=nil, path_prefix=nil)
1309
- params = {
1310
- 'cursor' => cursor,
1311
- 'path_prefix' => path_prefix,
1312
- }
1313
-
1314
- response = @session.do_post "/delta", params
1315
- Dropbox::parse_response(response)
1316
- end
1317
-
1318
- # Download a thumbnail (helper method - don't call this directly).
1319
- #
1320
- # Args:
1321
- # * +from_path+: The path to the file to be thumbnailed.
1322
- # * +size+: A string describing the desired thumbnail size. See thumbnail()
1323
- # for details.
1324
- #
1325
- # Returns:
1326
- # * The HTTPResponse for the thumbnail request.
1327
- def thumbnail_impl(from_path, size='large') # :nodoc:
1328
- path = "/thumbnails/#{@root}#{format_path(from_path, true)}"
1329
- params = {
1330
- "size" => size
1331
- }
1332
- headers = nil
1333
- content_server = true
1334
- @session.do_get path, params, headers, content_server
1335
- end
1336
- private :thumbnail_impl
1337
-
1338
-
1339
- # Creates and returns a copy ref for a specific file. The copy ref can be
1340
- # used to instantly copy that file to the Dropbox of another account.
1341
- #
1342
- # Args:
1343
- # * +path+: The path to the file for a copy ref to be created on.
1344
- #
1345
- # Returns:
1346
- # * A Hash object that looks like the following example:
1347
- # {"expires"=>"Fri, 31 Jan 2042 21:01:05 +0000", "copy_ref"=>"z1X6ATl6aWtzOGq0c3g5Ng"}
1348
- def create_copy_ref(path)
1349
- path = "/copy_ref/#{@root}#{format_path(path)}"
1350
- response = @session.do_get path
1351
- Dropbox::parse_response(response)
1352
- end
1353
-
1354
- # Adds the file referenced by the copy ref to the specified path
1355
- #
1356
- # Args:
1357
- # * +copy_ref+: A copy ref string that was returned from a create_copy_ref call.
1358
- # The copy_ref can be created from any other Dropbox account, or from the same account.
1359
- # * +to_path+: The path to where the file will be created.
1360
- #
1361
- # Returns:
1362
- # * A hash with the metadata of the new file.
1363
- def add_copy_ref(to_path, copy_ref)
1364
- params = {'from_copy_ref' => copy_ref,
1365
- 'to_path' => "#{to_path}",
1366
- 'root' => @root}
1367
-
1368
- response = @session.do_post "/fileops/copy", params
1369
- Dropbox::parse_response(response)
1370
- end
1371
-
1372
- #From the oauth spec plus "/". Slash should not be ecsaped
1373
- RESERVED_CHARACTERS = /[^a-zA-Z0-9\-\.\_\~\/]/ # :nodoc:
1374
-
1375
- def format_path(path, escape=true) # :nodoc:
1376
- path = path.gsub(/\/+/,"/")
1377
- # replace multiple slashes with a single one
1378
-
1379
- path = path.gsub(/^\/?/,"/")
1380
- # ensure the path starts with a slash
1381
-
1382
- path.gsub(/\/?$/,"")
1383
- # ensure the path doesn't end with a slash
1384
-
1385
- return URI.escape(path, RESERVED_CHARACTERS) if escape
1386
- path
1387
- end
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