dropbox-sdk 1.6.4 → 1.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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