lib/dropbox_sdk.rb in dropbox-sdk-1.6.4 vs lib/dropbox_sdk.rb in dropbox-sdk-1.6.5

- old
+ new

@@ -6,699 +6,706 @@ require 'base64' require 'securerandom' require 'pp' module Dropbox # :nodoc: - API_SERVER = "api.dropbox.com" - API_CONTENT_SERVER = "api-content.dropbox.com" - WEB_SERVER = "www.dropbox.com" + API_SERVER = "api.dropbox.com" + API_CONTENT_SERVER = "api-content.dropbox.com" + API_NOTIFY_SERVER = "api-notify.dropbox.com" + WEB_SERVER = "www.dropbox.com" - API_VERSION = 1 - SDK_VERSION = "1.6.4" + SERVERS = { + :api => API_SERVER, + :content => API_CONTENT_SERVER, + :notify => API_NOTIFY_SERVER, + :web => WEB_SERVER + } - TRUSTED_CERT_FILE = File.join(File.dirname(__FILE__), 'trusted-certs.crt') + API_VERSION = 1 + SDK_VERSION = "1.6.5" - def self.clean_params(params) - r = {} - params.each do |k,v| - r[k] = v.to_s if not v.nil? - end - r - end + TRUSTED_CERT_FILE = File.join(File.dirname(__FILE__), 'trusted-certs.crt') - def self.make_query_string(params) - clean_params(params).collect {|k,v| - CGI.escape(k) + "=" + CGI.escape(v) - }.join("&") + def self.clean_params(params) + r = {} + params.each do |k, v| + r[k] = v.to_s if not v.nil? end + r + end - def self.verify_ssl_certificate(preverify_ok, ssl_context) - if preverify_ok != true || ssl_context.error != 0 - err_msg = "SSL Verification failed -- Preverify: #{preverify_ok}, Error: #{ssl_context.error_string} (#{ssl_context.error})" - raise OpenSSL::SSL::SSLError.new(err_msg) - end - true - end + def self.make_query_string(params) + clean_params(params).collect {|k, v| + CGI.escape(k) + "=" + CGI.escape(v) + }.join("&") + end - def self.do_http(uri, request) # :nodoc: + def self.do_http(uri, request) # :nodoc: - http = Net::HTTP.new(uri.host, uri.port) + http = Net::HTTP.new(uri.host, uri.port) - http.use_ssl = true - http.verify_mode = OpenSSL::SSL::VERIFY_PEER - http.ca_file = Dropbox::TRUSTED_CERT_FILE + http.use_ssl = true + http.verify_mode = OpenSSL::SSL::VERIFY_PEER + http.ca_file = Dropbox::TRUSTED_CERT_FILE + http.read_timeout = 600 - if RUBY_VERSION >= '1.9' - # SSL protocol and ciphersuite settings are supported strating with version 1.9 - http.ssl_version = 'TLSv1' - http.ciphers = 'ECDHE-RSA-AES256-GCM-SHA384:'\ - 'ECDHE-RSA-AES256-SHA384:'\ - 'ECDHE-RSA-AES256-SHA:'\ - 'ECDHE-RSA-AES128-GCM-SHA256:'\ - 'ECDHE-RSA-AES128-SHA256:'\ - 'ECDHE-RSA-AES128-SHA:'\ - 'ECDHE-RSA-RC4-SHA:'\ - 'DHE-RSA-AES256-GCM-SHA384:'\ - 'DHE-RSA-AES256-SHA256:'\ - 'DHE-RSA-AES256-SHA:'\ - 'DHE-RSA-AES128-GCM-SHA256:'\ - 'DHE-RSA-AES128-SHA256:'\ - 'DHE-RSA-AES128-SHA:'\ - 'AES256-GCM-SHA384:'\ - 'AES256-SHA256:'\ - 'AES256-SHA:'\ - 'AES128-GCM-SHA256:'\ - 'AES128-SHA256:'\ - 'AES128-SHA' - end + if RUBY_VERSION >= '1.9' + # SSL protocol and ciphersuite settings are supported strating with version 1.9 + http.ssl_version = 'TLSv1' + http.ciphers = 'ECDHE-RSA-AES256-GCM-SHA384:'\ + 'ECDHE-RSA-AES256-SHA384:'\ + 'ECDHE-RSA-AES256-SHA:'\ + 'ECDHE-RSA-AES128-GCM-SHA256:'\ + 'ECDHE-RSA-AES128-SHA256:'\ + 'ECDHE-RSA-AES128-SHA:'\ + 'ECDHE-RSA-RC4-SHA:'\ + 'DHE-RSA-AES256-GCM-SHA384:'\ + 'DHE-RSA-AES256-SHA256:'\ + 'DHE-RSA-AES256-SHA:'\ + 'DHE-RSA-AES128-GCM-SHA256:'\ + 'DHE-RSA-AES128-SHA256:'\ + 'DHE-RSA-AES128-SHA:'\ + 'AES256-GCM-SHA384:'\ + 'AES256-SHA256:'\ + 'AES256-SHA:'\ + 'AES128-GCM-SHA256:'\ + 'AES128-SHA256:'\ + 'AES128-SHA' + end - # Important security note! - # Some Ruby versions (e.g. the one that ships with OS X) do not raise an exception if certificate validation fails. - # We therefore have to add a custom callback to ensure that invalid certs are not accepted - # See https://www.braintreepayments.com/braintrust/sslsocket-verify_mode-doesnt-verify - # You can comment out this code in case your Ruby version is not vulnerable - http.verify_callback = proc do |preverify_ok, ssl_context| - Dropbox::verify_ssl_certificate(preverify_ok, ssl_context) - end + # Important security note! + # Some Ruby versions (e.g. the one that ships with OS X) do not raise + # an exception if certificate validation fails. We therefore have to + # add a custom callback to ensure that invalid certs are not accepted. + # Some specific error codes are let through, so we change the error + # code to make sure that Ruby throws an exception if certificate + # validation fails. + # + # See the man page for 'verify' for more information on error codes. + # + # You can comment out this code if your Ruby version is not vulnerable. + http.verify_callback = proc do |preverify_ok, ssl_context| + # 0 is the error code for success + if preverify_ok && ssl_context.error == 0 + true + else + # 7 is the error code for certification signature failure + ssl_context.error = 7 + false + end + end - #We use this to better understand how developers are using our SDKs. - request['User-Agent'] = "OfficialDropboxRubySDK/#{Dropbox::SDK_VERSION}" + #We use this to better understand how developers are using our SDKs. + request['User-Agent'] = "OfficialDropboxRubySDK/#{Dropbox::SDK_VERSION}" - begin - http.request(request) - rescue OpenSSL::SSL::SSLError => e - raise DropboxError.new("SSL error connecting to Dropbox. " + - "There may be a problem with the set of certificates in \"#{Dropbox::TRUSTED_CERT_FILE}\". #{e.message}") - end + begin + http.request(request) + rescue OpenSSL::SSL::SSLError => e + raise DropboxError.new("SSL error connecting to Dropbox. " + + "There may be a problem with the set of certificates in \"#{Dropbox::TRUSTED_CERT_FILE}\". #{e.message}") end + end - # Parse response. You probably shouldn't be calling this directly. This takes responses from the server - # and parses them. It also checks for errors and raises exceptions with the appropriate messages. - def self.parse_response(response, raw=false) # :nodoc: - if response.kind_of?(Net::HTTPServerError) - raise DropboxError.new("Dropbox Server Error: #{response} - #{response.body}", response) - elsif response.kind_of?(Net::HTTPUnauthorized) - raise DropboxAuthError.new("User is not authenticated.", response) - elsif not response.kind_of?(Net::HTTPSuccess) - begin - d = JSON.parse(response.body) - rescue - raise DropboxError.new("Dropbox Server Error: body=#{response.body}", response) - end - if d['user_error'] and d['error'] - raise DropboxError.new(d['error'], response, d['user_error']) #user_error is translated - elsif d['error'] - raise DropboxError.new(d['error'], response) - else - raise DropboxError.new(response.body, response) - end - end + # Parse response. You probably shouldn't be calling this directly. This takes responses from the server + # and parses them. It also checks for errors and raises exceptions with the appropriate messages. + def self.parse_response(response, raw=false) # :nodoc: + if response.is_a?(Net::HTTPServerError) + raise DropboxError.new("Dropbox Server Error: #{response} - #{response.body}", response) + elsif response.is_a?(Net::HTTPUnauthorized) + raise DropboxAuthError.new("User is not authenticated.", response) + elsif !response.is_a?(Net::HTTPSuccess) + begin + d = JSON.parse(response.body) + rescue + raise DropboxError.new("Dropbox Server Error: body=#{response.body}", response) + end + if d['user_error'] and d['error'] + raise DropboxError.new(d['error'], response, d['user_error']) #user_error is translated + elsif d['error'] + raise DropboxError.new(d['error'], response) + else + raise DropboxError.new(response.body, response) + end + end - return response.body if raw + return response.body if raw - begin - return JSON.parse(response.body) - rescue JSON::ParserError - raise DropboxError.new("Unable to parse JSON response: #{response.body}", response) - end + begin + return JSON.parse(response.body) + rescue JSON::ParserError + raise DropboxError.new("Unable to parse JSON response: #{response.body}", response) end + end - # A string comparison function that is resistant to timing attacks. If you're comparing a - # string you got from the outside world with a string that is supposed to be a secret, use - # this function to check equality. - def self.safe_string_equals(a, b) - if a.length != b.length - false - else - a.chars.zip(b.chars).map {|ac,bc| ac == bc}.all? - end + # A string comparison function that is resistant to timing attacks. If you're comparing a + # string you got from the outside world with a string that is supposed to be a secret, use + # this function to check equality. + def self.safe_string_equals(a, b) + if a.length != b.length + false + else + a.chars.zip(b.chars).map {|ac,bc| ac == bc}.all? end + end end class DropboxSessionBase # :nodoc: - attr_writer :locale + attr_writer :locale - def initialize(locale) - @locale = locale - end + def initialize(locale) + @locale = locale + end - private + private - def build_url(path, content_server) - port = 443 - host = content_server ? Dropbox::API_CONTENT_SERVER : Dropbox::API_SERVER - full_path = "/#{Dropbox::API_VERSION}#{path}" - return URI::HTTPS.build({:host => host, :path => full_path}) - end + def build_url(path, server) + port = 443 + host = Dropbox::SERVERS[server] + full_path = "/#{Dropbox::API_VERSION}#{path}" + return URI::HTTPS.build({:host => host, :path => full_path}) + end - def build_url_with_params(path, params, content_server) # :nodoc: - target = build_url(path, content_server) - params['locale'] = @locale - target.query = Dropbox::make_query_string(params) - return target - end + def build_url_with_params(path, params, server) # :nodoc: + target = build_url(path, server) + params['locale'] = @locale + target.query = Dropbox::make_query_string(params) + return target + end - protected + protected - def do_http(uri, request) # :nodoc: - sign_request(request) - Dropbox::do_http(uri, request) - end + def do_http(uri, request) # :nodoc: + sign_request(request) + Dropbox::do_http(uri, request) + end - public + public - def do_get(path, params=nil, headers=nil, content_server=false) # :nodoc: - params ||= {} - assert_authorized - uri = build_url_with_params(path, params, content_server) - do_http(uri, Net::HTTP::Get.new(uri.request_uri)) - end + def do_get(path, params=nil, server=:api) # :nodoc: + params ||= {} + assert_authorized + uri = build_url_with_params(path, params, server) + do_http(uri, Net::HTTP::Get.new(uri.request_uri)) + end - def do_http_with_body(uri, request, body) - if body != nil - if body.is_a?(Hash) - request.set_form_data(Dropbox::clean_params(body)) - elsif body.respond_to?(:read) - if body.respond_to?(:length) - request["Content-Length"] = body.length.to_s - elsif body.respond_to?(:stat) && body.stat.respond_to?(:size) - request["Content-Length"] = body.stat.size.to_s - else - raise ArgumentError, "Don't know how to handle 'body' (responds to 'read' but not to 'length' or 'stat.size')." - end - request.body_stream = body - else - s = body.to_s - request["Content-Length"] = s.length - request.body = s - end + def do_http_with_body(uri, request, body) + if body != nil + if body.is_a?(Hash) + request.set_form_data(Dropbox::clean_params(body)) + elsif body.respond_to?(:read) + if body.respond_to?(:length) + request["Content-Length"] = body.length.to_s + elsif body.respond_to?(:stat) && body.stat.respond_to?(:size) + request["Content-Length"] = body.stat.size.to_s + else + raise ArgumentError, "Don't know how to handle 'body' (responds to 'read' but not to 'length' or 'stat.size')." end - do_http(uri, request) + request.body_stream = body + else + s = body.to_s + request["Content-Length"] = s.length + request.body = s + end end + do_http(uri, request) + end - def do_post(path, params=nil, headers=nil, content_server=false) # :nodoc: - params ||= {} - assert_authorized - uri = build_url(path, content_server) - params['locale'] = @locale - do_http_with_body(uri, Net::HTTP::Post.new(uri.request_uri, headers), params) - end + def do_post(path, params=nil, headers=nil, server=:api) # :nodoc: + params ||= {} + assert_authorized + uri = build_url(path, server) + params['locale'] = @locale + do_http_with_body(uri, Net::HTTP::Post.new(uri.request_uri, headers), params) + end - def do_put(path, params=nil, headers=nil, body=nil, content_server=false) # :nodoc: - params ||= {} - assert_authorized - uri = build_url_with_params(path, params, content_server) - do_http_with_body(uri, Net::HTTP::Put.new(uri.request_uri, headers), body) - end + def do_put(path, params=nil, headers=nil, body=nil, server=:api) # :nodoc: + params ||= {} + assert_authorized + uri = build_url_with_params(path, params, server) + do_http_with_body(uri, Net::HTTP::Put.new(uri.request_uri, headers), body) + end end # DropboxSession is responsible for holding OAuth 1 information. It knows how to take your consumer key and secret # and request an access token, an authorize url, and get an access token. You just need to pass it to # DropboxClient after its been authorized. class DropboxSession < DropboxSessionBase # :nodoc: - # * consumer_key - Your Dropbox application's "app key". - # * consumer_secret - Your Dropbox application's "app secret". - def initialize(consumer_key, consumer_secret, locale=nil) - super(locale) - @consumer_key = consumer_key - @consumer_secret = consumer_secret - @request_token = nil - @access_token = nil - end + # * consumer_key - Your Dropbox application's "app key". + # * consumer_secret - Your Dropbox application's "app secret". + def initialize(consumer_key, consumer_secret, locale=nil) + super(locale) + @consumer_key = consumer_key + @consumer_secret = consumer_secret + @request_token = nil + @access_token = nil + end - private + private - def build_auth_header(token) # :nodoc: - header = "OAuth oauth_version=\"1.0\", oauth_signature_method=\"PLAINTEXT\", " + - "oauth_consumer_key=\"#{URI.escape(@consumer_key)}\", " - if token - key = URI.escape(token.key) - secret = URI.escape(token.secret) - header += "oauth_token=\"#{key}\", oauth_signature=\"#{URI.escape(@consumer_secret)}&#{secret}\"" - else - header += "oauth_signature=\"#{URI.escape(@consumer_secret)}&\"" - end - header + def build_auth_header(token) # :nodoc: + header = "OAuth oauth_version=\"1.0\", oauth_signature_method=\"PLAINTEXT\", " + + "oauth_consumer_key=\"#{URI.escape(@consumer_key)}\", " + if token + key = URI.escape(token.key) + secret = URI.escape(token.secret) + header += "oauth_token=\"#{key}\", oauth_signature=\"#{URI.escape(@consumer_secret)}&#{secret}\"" + else + header += "oauth_signature=\"#{URI.escape(@consumer_secret)}&\"" end + header + end - def do_get_with_token(url, token, headers=nil) # :nodoc: - uri = URI.parse(url) - request = Net::HTTP::Get.new(uri.request_uri) - request.add_field('Authorization', build_auth_header(token)) - Dropbox::do_http(uri, request) - end + def do_get_with_token(url, token) # :nodoc: + uri = URI.parse(url) + request = Net::HTTP::Get.new(uri.request_uri) + request.add_field('Authorization', build_auth_header(token)) + Dropbox::do_http(uri, request) + end - protected + protected - def sign_request(request) # :nodoc: - request.add_field('Authorization', build_auth_header(@access_token)) - end + def sign_request(request) # :nodoc: + request.add_field('Authorization', build_auth_header(@access_token)) + end - public + public - def get_token(url_end, input_token, error_message_prefix) #: nodoc: - response = do_get_with_token("https://#{Dropbox::API_SERVER}:443/#{Dropbox::API_VERSION}/oauth#{url_end}", input_token) - if not response.kind_of?(Net::HTTPSuccess) # it must be a 200 - raise DropboxAuthError.new("#{error_message_prefix} Server returned #{response.code}: #{response.message}.", response) - end - parts = CGI.parse(response.body) - - if !parts.has_key? "oauth_token" and parts["oauth_token"].length != 1 - raise DropboxAuthError.new("Invalid response from #{url_end}: missing \"oauth_token\" parameter: #{response.body}", response) - end - if !parts.has_key? "oauth_token_secret" and parts["oauth_token_secret"].length != 1 - raise DropboxAuthError.new("Invalid response from #{url_end}: missing \"oauth_token\" parameter: #{response.body}", response) - end - - OAuthToken.new(parts["oauth_token"][0], parts["oauth_token_secret"][0]) + def get_token(url_end, input_token, error_message_prefix) #: nodoc: + response = do_get_with_token("https://#{Dropbox::API_SERVER}:443/#{Dropbox::API_VERSION}/oauth#{url_end}", input_token) + if not response.kind_of?(Net::HTTPSuccess) # it must be a 200 + raise DropboxAuthError.new("#{error_message_prefix} Server returned #{response.code}: #{response.message}.", response) end + parts = CGI.parse(response.body) - # This returns a request token. Requests one from the dropbox server using the provided application key and secret if nessecary. - def get_request_token() - @request_token ||= get_token("/request_token", nil, "Error getting request token. Is your app key and secret correctly set?") + if !parts.has_key? "oauth_token" and parts["oauth_token"].length != 1 + raise DropboxAuthError.new("Invalid response from #{url_end}: missing \"oauth_token\" parameter: #{response.body}", response) end + if !parts.has_key? "oauth_token_secret" and parts["oauth_token_secret"].length != 1 + raise DropboxAuthError.new("Invalid response from #{url_end}: missing \"oauth_token\" parameter: #{response.body}", response) + end - # This returns a URL that your user must visit to grant - # permissions to this application. - def get_authorize_url(callback=nil) - get_request_token() + OAuthToken.new(parts["oauth_token"][0], parts["oauth_token_secret"][0]) + end - url = "/#{Dropbox::API_VERSION}/oauth/authorize?oauth_token=#{URI.escape(@request_token.key)}" - if callback - url += "&oauth_callback=#{URI.escape(callback)}" - end - if @locale - url += "&locale=#{URI.escape(@locale)}" - end + # This returns a request token. Requests one from the dropbox server using the provided application key and secret if nessecary. + def get_request_token() + @request_token ||= get_token("/request_token", nil, "Error getting request token. Is your app key and secret correctly set?") + end - "https://#{Dropbox::WEB_SERVER}#{url}" - end + # This returns a URL that your user must visit to grant + # permissions to this application. + def get_authorize_url(callback=nil) + get_request_token() - # Clears the access_token - def clear_access_token - @access_token = nil + url = "/#{Dropbox::API_VERSION}/oauth/authorize?oauth_token=#{URI.escape(@request_token.key)}" + if callback + url += "&oauth_callback=#{URI.escape(callback)}" end - - # Returns the request token, or nil if one hasn't been acquired yet. - def request_token - @request_token + if @locale + url += "&locale=#{URI.escape(@locale)}" end - # Returns the access token, or nil if one hasn't been acquired yet. - def access_token - @access_token - end + "https://#{Dropbox::WEB_SERVER}#{url}" + end - # Given a saved request token and secret, set this location's token and secret - # * token - this is the request token - # * secret - this is the request token secret - def set_request_token(key, secret) - @request_token = OAuthToken.new(key, secret) - end + # Clears the access_token + def clear_access_token + @access_token = nil + end - # Given a saved access token and secret, you set this Session to use that token and secret - # * token - this is the access token - # * secret - this is the access token secret - def set_access_token(key, secret) - @access_token = OAuthToken.new(key, secret) - end + # Returns the request token, or nil if one hasn't been acquired yet. + def request_token + @request_token + end - # Returns the access token. If this DropboxSession doesn't yet have an access_token, it requests one - # using the request_token generate from your app's token and secret. This request will fail unless - # your user has gone to the authorize_url and approved your request - def get_access_token - return @access_token if authorized? + # Returns the access token, or nil if one hasn't been acquired yet. + def access_token + @access_token + end - if @request_token.nil? - raise RuntimeError.new("No request token. You must set this or get an authorize url first.") - end + # Given a saved request token and secret, set this location's token and secret + # * token - this is the request token + # * secret - this is the request token secret + def set_request_token(key, secret) + @request_token = OAuthToken.new(key, secret) + end - @access_token = get_token("/access_token", @request_token, "Couldn't get access token.") - end + # Given a saved access token and secret, you set this Session to use that token and secret + # * token - this is the access token + # * secret - this is the access token secret + def set_access_token(key, secret) + @access_token = OAuthToken.new(key, secret) + end - # If we have an access token, then do nothing. If not, throw a RuntimeError. - def assert_authorized - unless authorized? - raise RuntimeError.new('Session does not yet have a request token') - end - end + # Returns the access token. If this DropboxSession doesn't yet have an access_token, it requests one + # using the request_token generate from your app's token and secret. This request will fail unless + # your user has gone to the authorize_url and approved your request + def get_access_token + return @access_token if authorized? - # Returns true if this Session has been authorized and has an access_token. - def authorized? - !!@access_token + if @request_token.nil? + raise RuntimeError.new("No request token. You must set this or get an authorize url first.") end - # serialize the DropboxSession. - # At DropboxSession's state is capture in three key/secret pairs. Consumer, request, and access. - # Serialize returns these in a YAML string, generated from a converted array of the form: - # [consumer_key, consumer_secret, request_token.token, request_token.secret, access_token.token, access_token.secret] - # access_token is only included if it already exists in the DropboxSesssion - def serialize - toreturn = [] - if @access_token - toreturn.push @access_token.secret, @access_token.key - end + @access_token = get_token("/access_token", @request_token, "Couldn't get access token.") + end - get_request_token + # If we have an access token, then do nothing. If not, throw a RuntimeError. + def assert_authorized + unless authorized? + raise RuntimeError.new('Session does not yet have a request token') + end + end - toreturn.push @request_token.secret, @request_token.key - toreturn.push @consumer_secret, @consumer_key + # Returns true if this Session has been authorized and has an access_token. + def authorized? + !!@access_token + end - toreturn.to_yaml + # serialize the DropboxSession. + # At DropboxSession's state is capture in three key/secret pairs. Consumer, request, and access. + # Serialize returns these in a YAML string, generated from a converted array of the form: + # [consumer_key, consumer_secret, request_token.token, request_token.secret, access_token.token, access_token.secret] + # access_token is only included if it already exists in the DropboxSesssion + def serialize + toreturn = [] + if @access_token + toreturn.push @access_token.secret, @access_token.key end - # Takes a serialized DropboxSession YAML String and returns a new DropboxSession object - def self.deserialize(ser) - ser = YAML::load(ser) - session = DropboxSession.new(ser.pop, ser.pop) - session.set_request_token(ser.pop, ser.pop) + get_request_token - if ser.length > 0 - session.set_access_token(ser.pop, ser.pop) - end - session + toreturn.push @request_token.secret, @request_token.key + toreturn.push @consumer_secret, @consumer_key + + toreturn.to_yaml + end + + # Takes a serialized DropboxSession YAML String and returns a new DropboxSession object + def self.deserialize(ser) + ser = YAML::load(ser) + session = DropboxSession.new(ser.pop, ser.pop) + session.set_request_token(ser.pop, ser.pop) + + if ser.length > 0 + session.set_access_token(ser.pop, ser.pop) end + session + end end class DropboxOAuth2Session < DropboxSessionBase # :nodoc: - def initialize(oauth2_access_token, locale=nil) - super(locale) - if not oauth2_access_token.is_a?(String) - raise "bad type for oauth2_access_token (expecting String)" - end - @access_token = oauth2_access_token + def initialize(oauth2_access_token, locale=nil) + super(locale) + if not oauth2_access_token.is_a?(String) + raise "bad type for oauth2_access_token (expecting String)" end + @access_token = oauth2_access_token + end - def assert_authorized - true - end + def assert_authorized + true + end - protected + protected - def sign_request(request) # :nodoc: - request.add_field('Authorization', 'Bearer ' + @access_token) - end + def sign_request(request) # :nodoc: + request.add_field('Authorization', 'Bearer ' + @access_token) + end end # Base class for the two OAuth 2 authorization helpers. class DropboxOAuth2FlowBase # :nodoc: - def initialize(consumer_key, consumer_secret, locale=nil) - if not consumer_key.is_a?(String) - raise ArgumentError, "consumer_key must be a String, got #{consumer_key.inspect}" - end - if not consumer_secret.is_a?(String) - raise ArgumentError, "consumer_secret must be a String, got #{consumer_secret.inspect}" - end - if not (locale.nil? or locale.is_a?(String)) - raise ArgumentError, "locale must be a String or nil, got #{locale.inspect}" - end - @consumer_key = consumer_key - @consumer_secret = consumer_secret - @locale = locale + def initialize(consumer_key, consumer_secret, locale=nil) + if not consumer_key.is_a?(String) + raise ArgumentError, "consumer_key must be a String, got #{consumer_key.inspect}" end + if not consumer_secret.is_a?(String) + raise ArgumentError, "consumer_secret must be a String, got #{consumer_secret.inspect}" + end + if not (locale.nil? or locale.is_a?(String)) + raise ArgumentError, "locale must be a String or nil, got #{locale.inspect}" + end + @consumer_key = consumer_key + @consumer_secret = consumer_secret + @locale = locale + end - def _get_authorize_url(redirect_uri, state) - params = { - "client_id" => @consumer_key, - "response_type" => "code", - "redirect_uri" => redirect_uri, - "state" => state, - "locale" => @locale, - } + def _get_authorize_url(redirect_uri, state) + params = { + "client_id" => @consumer_key, + "response_type" => "code", + "redirect_uri" => redirect_uri, + "state" => state, + "locale" => @locale, + } - host = Dropbox::WEB_SERVER - path = "/#{Dropbox::API_VERSION}/oauth2/authorize" + host = Dropbox::WEB_SERVER + path = "/#{Dropbox::API_VERSION}/oauth2/authorize" - target = URI::Generic.new("https", nil, host, nil, nil, path, nil, nil, nil) - target.query = Dropbox::make_query_string(params) + target = URI::Generic.new("https", nil, host, nil, nil, path, nil, nil, nil) + target.query = Dropbox::make_query_string(params) - target.to_s + target.to_s + end + + # Finish the OAuth 2 authorization process. If you used a redirect_uri, pass that in. + # Will return an access token string that you can use with DropboxClient. + def _finish(code, original_redirect_uri) + if not code.is_a?(String) + raise ArgumentError, "code must be a String" end - # Finish the OAuth 2 authorization process. If you used a redirect_uri, pass that in. - # Will return an access token string that you can use with DropboxClient. - def _finish(code, original_redirect_uri) - if not code.is_a?(String) - raise ArgumentError, "code must be a String" - end + uri = URI.parse("https://#{Dropbox::API_SERVER}/1/oauth2/token") + request = Net::HTTP::Post.new(uri.request_uri) + client_credentials = @consumer_key + ':' + @consumer_secret + request.add_field('Authorization', 'Basic ' + Base64.encode64(client_credentials).chomp("\n")) - uri = URI.parse("https://#{Dropbox::API_SERVER}/1/oauth2/token") - request = Net::HTTP::Post.new(uri.request_uri) - client_credentials = @consumer_key + ':' + @consumer_secret - request.add_field('Authorization', 'Basic ' + Base64.encode64(client_credentials).chomp("\n")) + params = { + "grant_type" => "authorization_code", + "code" => code, + "redirect_uri" => original_redirect_uri, + "locale" => @locale, + } - params = { - "grant_type" => "authorization_code", - "code" => code, - "redirect_uri" => original_redirect_uri, - "locale" => @locale, - } + request.set_form_data(Dropbox::clean_params(params)) - request.set_form_data(Dropbox::clean_params(params)) + response = Dropbox::do_http(uri, request) - response = Dropbox::do_http(uri, request) - - j = Dropbox::parse_response(response) - ["token_type", "access_token", "uid"].each { |k| - if not j.has_key?(k) - raise DropboxError.new("Bad response from /token: missing \"#{k}\".") - end - if not j[k].is_a?(String) - raise DropboxError.new("Bad response from /token: field \"#{k}\" is not a string.") - end - } - if j["token_type"] != "bearer" and j["token_type"] != "Bearer" - raise DropboxError.new("Bad response from /token: \"token_type\" is \"#{token_type}\".") - end - - return j['access_token'], j['uid'] + j = Dropbox::parse_response(response) + ["token_type", "access_token", "uid"].each { |k| + if not j.has_key?(k) + raise DropboxError.new("Bad response from /token: missing \"#{k}\".") + end + if not j[k].is_a?(String) + raise DropboxError.new("Bad response from /token: field \"#{k}\" is not a string.") + end + } + if j["token_type"] != "bearer" and j["token_type"] != "Bearer" + raise DropboxError.new("Bad response from /token: \"token_type\" is \"#{token_type}\".") end + + return j['access_token'], j['uid'] + end end # OAuth 2 authorization helper for apps that can't provide a redirect URI # (such as the command line example apps). class DropboxOAuth2FlowNoRedirect < DropboxOAuth2FlowBase - # * consumer_key: Your Dropbox API app's "app key" - # * consumer_secret: Your Dropbox API app's "app secret" - # * locale: The locale of the user currently using your app. - def initialize(consumer_key, consumer_secret, locale=nil) - super(consumer_key, consumer_secret, locale) - end + # * consumer_key: Your Dropbox API app's "app key" + # * consumer_secret: Your Dropbox API app's "app secret" + # * locale: The locale of the user currently using your app. + def initialize(consumer_key, consumer_secret, locale=nil) + super(consumer_key, consumer_secret, locale) + end - # Returns a authorization_url, which is a page on Dropbox's website. Have the user - # visit this URL and approve your app. - def start() - _get_authorize_url(nil, nil) - end + # Returns a authorization_url, which is a page on Dropbox's website. Have the user + # visit this URL and approve your app. + def start() + _get_authorize_url(nil, nil) + end - # If the user approves your app, they will be presented with an "authorization code". - # Have the user copy/paste that authorization code into your app and then call this - # method to get an access token. - # - # Returns a two-entry list (access_token, user_id) - # * access_token is an access token string that can be passed to DropboxClient. - # * user_id is the Dropbox user ID of the user that just approved your app. - def finish(code) - _finish(code, nil) - end + # If the user approves your app, they will be presented with an "authorization code". + # Have the user copy/paste that authorization code into your app and then call this + # method to get an access token. + # + # Returns a two-entry list (access_token, user_id) + # * access_token is an access token string that can be passed to DropboxClient. + # * user_id is the Dropbox user ID of the user that just approved your app. + def finish(code) + _finish(code, nil) + end end # The standard OAuth 2 authorization helper. Use this if you're writing a web app. class DropboxOAuth2Flow < DropboxOAuth2FlowBase - # * consumer_key: Your Dropbox API app's "app key" - # * consumer_secret: Your Dropbox API app's "app secret" - # * redirect_uri: The URI that the Dropbox server will redirect the user to after the user - # finishes authorizing your app. This URI must be HTTPs-based and pre-registered with - # the Dropbox servers, though localhost URIs are allowed without pre-registration and can - # be either HTTP or HTTPS. - # * session: A hash that represents the current web app session (will be used to save the CSRF - # token) - # * csrf_token_key: The key to use when storing the CSRF token in the session (for example, - # :dropbox_auth_csrf_token) - # * locale: The locale of the user currently using your app (ex: "en" or "en_US"). - def initialize(consumer_key, consumer_secret, redirect_uri, session, csrf_token_session_key, locale=nil) - super(consumer_key, consumer_secret, locale) - if not redirect_uri.is_a?(String) - raise ArgumentError, "redirect_uri must be a String, got #{consumer_secret.inspect}" - end - @redirect_uri = redirect_uri - @session = session - @csrf_token_session_key = csrf_token_session_key + # * consumer_key: Your Dropbox API app's "app key" + # * consumer_secret: Your Dropbox API app's "app secret" + # * redirect_uri: The URI that the Dropbox server will redirect the user to after the user + # finishes authorizing your app. This URI must be HTTPs-based and pre-registered with + # the Dropbox servers, though localhost URIs are allowed without pre-registration and can + # be either HTTP or HTTPS. + # * session: A hash that represents the current web app session (will be used to save the CSRF + # token) + # * csrf_token_key: The key to use when storing the CSRF token in the session (for example, + # :dropbox_auth_csrf_token) + # * locale: The locale of the user currently using your app (ex: "en" or "en_US"). + def initialize(consumer_key, consumer_secret, redirect_uri, session, csrf_token_session_key, locale=nil) + super(consumer_key, consumer_secret, locale) + if not redirect_uri.is_a?(String) + raise ArgumentError, "redirect_uri must be a String, got #{consumer_secret.inspect}" end + @redirect_uri = redirect_uri + @session = session + @csrf_token_session_key = csrf_token_session_key + end - # Starts the OAuth 2 authorizaton process, which involves redirecting the user to - # the returned "authorization URL" (a URL on the Dropbox website). When the user then - # either approves or denies your app access, Dropbox will redirect them to the - # redirect_uri you provided to the constructor, at which point you should call finish() - # to complete the process. - # - # This function will also save a CSRF token to the session and csrf_token_session_key - # you provided to the constructor. This CSRF token will be checked on finish() to prevent - # request forgery. - # - # * url_state: Any data you would like to keep in the URL through the authorization - # process. This exact value will be returned to you by finish(). - # - # Returns the URL to redirect the user to. - def start(url_state=nil) - unless url_state.nil? or url_state.is_a?(String) - raise ArgumentError, "url_state must be a String" - end + # Starts the OAuth 2 authorizaton process, which involves redirecting the user to + # the returned "authorization URL" (a URL on the Dropbox website). When the user then + # either approves or denies your app access, Dropbox will redirect them to the + # redirect_uri you provided to the constructor, at which point you should call finish() + # to complete the process. + # + # This function will also save a CSRF token to the session and csrf_token_session_key + # you provided to the constructor. This CSRF token will be checked on finish() to prevent + # request forgery. + # + # * url_state: Any data you would like to keep in the URL through the authorization + # process. This exact value will be returned to you by finish(). + # + # Returns the URL to redirect the user to. + def start(url_state=nil) + unless url_state.nil? or url_state.is_a?(String) + raise ArgumentError, "url_state must be a String" + end - csrf_token = SecureRandom.base64(16) - state = csrf_token - unless url_state.nil? - state += "|" + url_state - end - @session[@csrf_token_session_key] = csrf_token - - return _get_authorize_url(@redirect_uri, state) + csrf_token = SecureRandom.base64(16) + state = csrf_token + unless url_state.nil? + state += "|" + url_state end + @session[@csrf_token_session_key] = csrf_token - # Call this after the user has visited the authorize URL (see: start()), approved your app, - # and was redirected to your redirect URI. - # - # * query_params: The query params on the GET request to your redirect URI. - # - # Returns a tuple of (access_token, user_id, url_state). access_token can be used to - # construct a DropboxClient. user_id is the Dropbox user ID of the user that jsut approved - # your app. url_state is the value you originally passed in to start(). - # - # Can throw BadRequestError, BadStateError, CsrfError, NotApprovedError, - # ProviderError, and the standard DropboxError. - def finish(query_params) - csrf_token_from_session = @session[@csrf_token_session_key] + return _get_authorize_url(@redirect_uri, state) + end - # Check well-formedness of request. + # Call this after the user has visited the authorize URL (see: start()), approved your app, + # and was redirected to your redirect URI. + # + # * query_params: The query params on the GET request to your redirect URI. + # + # Returns a tuple of (access_token, user_id, url_state). access_token can be used to + # construct a DropboxClient. user_id is the Dropbox user ID of the user that jsut approved + # your app. url_state is the value you originally passed in to start(). + # + # Can throw BadRequestError, BadStateError, CsrfError, NotApprovedError, + # ProviderError, and the standard DropboxError. + def finish(query_params) + csrf_token_from_session = @session[@csrf_token_session_key] - state = query_params['state'] - if state.nil? - raise BadRequestError.new("Missing query parameter 'state'.") - end + # Check well-formedness of request. - error = query_params['error'] - error_description = query_params['error_description'] - code = query_params['code'] + state = query_params['state'] + if state.nil? + raise BadRequestError.new("Missing query parameter 'state'.") + end - if not error.nil? and not code.nil? - raise BadRequestError.new("Query parameters 'code' and 'error' are both set;" + - " only one must be set.") - end - if error.nil? and code.nil? - raise BadRequestError.new("Neither query parameter 'code' or 'error' is set.") - end + error = query_params['error'] + error_description = query_params['error_description'] + code = query_params['code'] - # Check CSRF token + if not error.nil? and not code.nil? + raise BadRequestError.new("Query parameters 'code' and 'error' are both set;" + + " only one must be set.") + end + if error.nil? and code.nil? + raise BadRequestError.new("Neither query parameter 'code' or 'error' is set.") + end - if csrf_token_from_session.nil? - raise BadStateError.new("Missing CSRF token in session."); - end - unless csrf_token_from_session.length > 20 - raise RuntimeError.new("CSRF token unexpectedly short: #{csrf_token_from_session.inspect}") - end + # Check CSRF token - split_pos = state.index('|') - if split_pos.nil? - given_csrf_token = state - url_state = nil + if csrf_token_from_session.nil? + raise BadStateError.new("Missing CSRF token in session."); + end + unless csrf_token_from_session.length > 20 + raise RuntimeError.new("CSRF token unexpectedly short: #{csrf_token_from_session.inspect}") + end + + split_pos = state.index('|') + if split_pos.nil? + given_csrf_token = state + url_state = nil + else + given_csrf_token, url_state = state.split('|', 2) + end + if not Dropbox::safe_string_equals(csrf_token_from_session, given_csrf_token) + raise CsrfError.new("Expected #{csrf_token_from_session.inspect}, " + + "got #{given_csrf_token.inspect}.") + end + @session.delete(@csrf_token_session_key) + + # Check for error identifier + + if not error.nil? + if error == 'access_denied' + # The user clicked "Deny" + if error_description.nil? + raise NotApprovedError.new("No additional description from Dropbox.") else - given_csrf_token, url_state = state.split('|', 2) + raise NotApprovedError.new("Additional description from Dropbox: #{error_description}") end - if not Dropbox::safe_string_equals(csrf_token_from_session, given_csrf_token) - raise CsrfError.new("Expected #{csrf_token_from_session.inspect}, " + - "got #{given_csrf_token.inspect}.") + else + # All other errors. + full_message = error + if not error_description.nil? + full_message += ": " + error_description end - @session.delete(@csrf_token_session_key) + raise ProviderError.new(full_message) + end + end - # Check for error identifier + # If everything went ok, make the network call to get an access token. - if not error.nil? - if error == 'access_denied' - # The user clicked "Deny" - if error_description.nil? - raise NotApprovedError.new("No additional description from Dropbox.") - else - raise NotApprovedError.new("Additional description from Dropbox: #{error_description}") - end - else - # All other errors. - full_message = error - if not error_description.nil? - full_message += ": " + error_description - end - raise ProviderError.new(full_message) - end - end + access_token, user_id = _finish(code, @redirect_uri) + return access_token, user_id, url_state + end - # If everything went ok, make the network call to get an access token. + # Thrown if the redirect URL was missing parameters or if the given parameters were not valid. + # + # The recommended action is to show an HTTP 400 error page. + class BadRequestError < Exception; end - access_token, user_id = _finish(code, @redirect_uri) - return access_token, user_id, url_state - end + # Thrown if all the parameters are correct, but there's no CSRF token in the session. This + # probably means that the session expired. + # + # The recommended action is to redirect the user's browser to try the approval process again. + class BadStateError < Exception; end - # Thrown if the redirect URL was missing parameters or if the given parameters were not valid. - # - # The recommended action is to show an HTTP 400 error page. - class BadRequestError < Exception; end + # Thrown if the given 'state' parameter doesn't contain the CSRF token from the user's session. + # This is blocked to prevent CSRF attacks. + # + # The recommended action is to respond with an HTTP 403 error page. + class CsrfError < Exception; end - # Thrown if all the parameters are correct, but there's no CSRF token in the session. This - # probably means that the session expired. - # - # The recommended action is to redirect the user's browser to try the approval process again. - class BadStateError < Exception; end + # The user chose not to approve your app. + class NotApprovedError < Exception; end - # Thrown if the given 'state' parameter doesn't contain the CSRF token from the user's session. - # This is blocked to prevent CSRF attacks. - # - # The recommended action is to respond with an HTTP 403 error page. - class CsrfError < Exception; end - - # The user chose not to approve your app. - class NotApprovedError < Exception; end - - # Dropbox redirected to your redirect URI with some unexpected error identifier and error - # message. - class ProviderError < Exception; end + # Dropbox redirected to your redirect URI with some unexpected error identifier and error + # message. + class ProviderError < Exception; end end # A class that represents either an OAuth request token or an OAuth access token. class OAuthToken # :nodoc: - def initialize(key, secret) - @key = key - @secret = secret - end - - def key - @key - end - - def secret - @secret - end + attr_reader :key, :secret + def initialize(key, secret) + @key = key + @secret = secret + end end # This is the usual error raised on any Dropbox related Errors class DropboxError < RuntimeError - attr_accessor :http_response, :error, :user_error - def initialize(error, http_response=nil, user_error=nil) - @error = error - @http_response = http_response - @user_error = user_error - end + attr_accessor :http_response, :error, :user_error + def initialize(error, http_response=nil, user_error=nil) + @error = error + @http_response = http_response + @user_error = user_error + end - def to_s - return "#{user_error} (#{error})" if user_error - "#{error}" - end + def to_s + return "#{user_error} (#{error})" if user_error + "#{error}" + end end # This is the error raised on Authentication failures. Usually this means # one of three things # * Your user failed to go to the authorize url and approve your application @@ -714,676 +721,714 @@ # Use this class to make Dropbox API calls. You'll need to obtain an OAuth 2 access token # first; you can get one using either DropboxOAuth2Flow or DropboxOAuth2FlowNoRedirect. class DropboxClient - # Args: - # * +oauth2_access_token+: Obtained via DropboxOAuth2Flow or DropboxOAuth2FlowNoRedirect. - # * +locale+: The user's current locale (used to localize error messages). - def initialize(oauth2_access_token, root="auto", locale=nil) - if oauth2_access_token.is_a?(String) - @session = DropboxOAuth2Session.new(oauth2_access_token, locale) - elsif oauth2_access_token.is_a?(DropboxSession) - @session = oauth2_access_token - @session.get_access_token - if not locale.nil? - @session.locale = locale - end - else - raise ArgumentError.new("oauth2_access_token doesn't have a valid type") - end + # Args: + # * +oauth2_access_token+: Obtained via DropboxOAuth2Flow or DropboxOAuth2FlowNoRedirect. + # * +locale+: The user's current locale (used to localize error messages). + def initialize(oauth2_access_token, root="auto", locale=nil) + if oauth2_access_token.is_a?(String) + @session = DropboxOAuth2Session.new(oauth2_access_token, locale) + elsif oauth2_access_token.is_a?(DropboxSession) + @session = oauth2_access_token + @session.get_access_token + if not locale.nil? + @session.locale = locale + end + else + raise ArgumentError.new("oauth2_access_token doesn't have a valid type") + end - @root = root.to_s # If they passed in a symbol, make it a string + @root = root.to_s # If they passed in a symbol, make it a string - if not ["dropbox","app_folder","auto"].include?(@root) - raise ArgumentError.new("root must be :dropbox, :app_folder, or :auto") - end - if @root == "app_folder" - #App Folder is the name of the access type, but for historical reasons - #sandbox is the URL root component that indicates this - @root = "sandbox" - end + if not ["dropbox","app_folder","auto"].include?(@root) + raise ArgumentError.new("root must be :dropbox, :app_folder, or :auto") end + if @root == "app_folder" + #App Folder is the name of the access type, but for historical reasons + #sandbox is the URL root component that indicates this + @root = "sandbox" + end + end - # Returns some information about the current user's Dropbox account (the "current user" - # is the user associated with the access token you're using). - # - # For a detailed description of what this call returns, visit: - # https://www.dropbox.com/developers/reference/api#account-info - def account_info() - response = @session.do_get "/account/info" - Dropbox::parse_response(response) + # Returns some information about the current user's Dropbox account (the "current user" + # is the user associated with the access token you're using). + # + # For a detailed description of what this call returns, visit: + # https://www.dropbox.com/developers/reference/api#account-info + def account_info() + response = @session.do_get "/account/info" + Dropbox::parse_response(response) + end + + # Disables the access token that this +DropboxClient+ is using. If this call + # succeeds, further API calls using this object will fail. + def disable_access_token + @session.do_post "/disable_access_token" + nil + end + + # If this +DropboxClient+ was created with an OAuth 1 access token, this method + # can be used to create an equivalent OAuth 2 access token. This can be used to + # upgrade your app's existing access tokens from OAuth 1 to OAuth 2. + def create_oauth2_access_token + if not @session.is_a?(DropboxSession) + raise ArgumentError.new("This call requires a DropboxClient that is configured with " \ + "an OAuth 1 access token.") end + response = @session.do_post "/oauth2/token_from_oauth1" + Dropbox::parse_response(response)['access_token'] + end - # Disables the access token that this +DropboxClient+ is using. If this call - # succeeds, further API calls using this object will fail. - def disable_access_token - @session.do_post "/disable_access_token" - nil + # Uploads a file to a server. This uses the HTTP PUT upload method for simplicity + # + # Args: + # * +to_path+: The directory path to upload the file to. If the destination + # directory does not yet exist, it will be created. + # * +file_obj+: A file-like object to upload. If you would like, you can + # pass a string as file_obj. + # * +overwrite+: Whether to overwrite an existing file at the given path. [default is False] + # If overwrite is False and a file already exists there, Dropbox + # will rename the upload to make sure it doesn't overwrite anything. + # You must check the returned metadata to know what this new name is. + # This field should only be True if your intent is to potentially + # clobber changes to a file that you don't know about. + # * +parent_rev+: The rev field from the 'parent' of this upload. [optional] + # If your intent is to update the file at the given path, you should + # pass the parent_rev parameter set to the rev value from the most recent + # metadata you have of the existing file at that path. If the server + # has a more recent version of the file at the specified path, it will + # automatically rename your uploaded file, spinning off a conflict. + # Using this parameter effectively causes the overwrite parameter to be ignored. + # The file will always be overwritten if you send the most-recent parent_rev, + # and it will never be overwritten you send a less-recent one. + # Returns: + # * a Hash containing the metadata of the newly uploaded file. The file may have a different + # name if it conflicted. + # + # Simple Example + # client = DropboxClient(oauth2_access_token) + # #session is a DropboxSession I've already authorized + # client.put_file('/test_file_on_dropbox', open('/tmp/test_file')) + # This will upload the "/tmp/test_file" from my computer into the root of my App's app folder + # and call it "test_file_on_dropbox". + # The file will not overwrite any pre-existing file. + def put_file(to_path, file_obj, overwrite=false, parent_rev=nil) + path = "/files_put/#{@root}#{format_path(to_path)}" + params = { + 'overwrite' => overwrite.to_s, + 'parent_rev' => parent_rev, + } + + headers = {"Content-Type" => "application/octet-stream"} + response = @session.do_put path, params, headers, file_obj, :content + + Dropbox::parse_response(response) + end + + # Returns a ChunkedUploader object. + # + # Args: + # * +file_obj+: The file-like object to be uploaded. Must support .read() + # * +total_size+: The total size of file_obj + def get_chunked_uploader(file_obj, total_size) + ChunkedUploader.new(self, file_obj, total_size) + end + + # ChunkedUploader is responsible for uploading a large file to Dropbox in smaller chunks. + # This allows large files to be uploaded and makes allows recovery during failure. + class ChunkedUploader + attr_accessor :file_obj, :total_size, :offset, :upload_id, :client + + def initialize(client, file_obj, total_size) + @client = client + @file_obj = file_obj + @total_size = total_size + @upload_id = nil + @offset = 0 end - # If this +DropboxClient+ was created with an OAuth 1 access token, this method - # can be used to create an equivalent OAuth 2 access token. This can be used to - # upgrade your app's existing access tokens from OAuth 1 to OAuth 2. - def create_oauth2_access_token - if not @session.is_a?(DropboxSession) - raise ArgumentError.new("This call requires a DropboxClient that is configured with " \ - "an OAuth 1 access token.") + # Uploads data from this ChunkedUploader's file_obj in chunks, until + # an error occurs. Throws an exception when an error occurs, and can + # be called again to resume the upload. + # + # Args: + # * +chunk_size+: The chunk size for each individual upload. Defaults to 4MB. + def upload(chunk_size=4*1024*1024) + last_chunk = nil + + while @offset < @total_size + if not last_chunk + last_chunk = @file_obj.read(chunk_size) end - response = @session.do_post "/oauth2/token_from_oauth1" - Dropbox::parse_response(response)['access_token'] + + resp = {} + begin + resp = Dropbox::parse_response(@client.partial_chunked_upload(last_chunk, @upload_id, @offset)) + last_chunk = nil + rescue SocketError => e + raise e + rescue SystemCallError => e + raise e + rescue DropboxError => e + raise e if e.http_response.nil? or e.http_response.code[0] == '5' + begin + resp = JSON.parse(e.http_response.body) + raise DropboxError.new('server response does not have offset key') unless resp.has_key? 'offset' + rescue JSON::ParserError + raise DropboxError.new("Unable to parse JSON response: #{e.http_response.body}") + end + end + + if resp.has_key? 'offset' and resp['offset'] > @offset + @offset += (resp['offset'] - @offset) if resp['offset'] + last_chunk = nil + end + @upload_id = resp['upload_id'] if resp['upload_id'] + end end - # Uploads a file to a server. This uses the HTTP PUT upload method for simplicity + # Completes a file upload # # Args: # * +to_path+: The directory path to upload the file to. If the destination # directory does not yet exist, it will be created. - # * +file_obj+: A file-like object to upload. If you would like, you can - # pass a string as file_obj. # * +overwrite+: Whether to overwrite an existing file at the given path. [default is False] # If overwrite is False and a file already exists there, Dropbox # will rename the upload to make sure it doesn't overwrite anything. # You must check the returned metadata to know what this new name is. # This field should only be True if your intent is to potentially # clobber changes to a file that you don't know about. - # * +parent_rev+: The rev field from the 'parent' of this upload. [optional] + # * parent_rev: The rev field from the 'parent' of this upload. # If your intent is to update the file at the given path, you should # pass the parent_rev parameter set to the rev value from the most recent # metadata you have of the existing file at that path. If the server # has a more recent version of the file at the specified path, it will # automatically rename your uploaded file, spinning off a conflict. # Using this parameter effectively causes the overwrite parameter to be ignored. # The file will always be overwritten if you send the most-recent parent_rev, # and it will never be overwritten you send a less-recent one. - # Returns: - # * a Hash containing the metadata of the newly uploaded file. The file may have a different - # name if it conflicted. # - # Simple Example - # client = DropboxClient(oauth2_access_token) - # #session is a DropboxSession I've already authorized - # client.put_file('/test_file_on_dropbox', open('/tmp/test_file')) - # This will upload the "/tmp/test_file" from my computer into the root of my App's app folder - # and call it "test_file_on_dropbox". - # The file will not overwrite any pre-existing file. - def put_file(to_path, file_obj, overwrite=false, parent_rev=nil) - path = "/files_put/#{@root}#{format_path(to_path)}" - params = { - 'overwrite' => overwrite.to_s, - 'parent_rev' => parent_rev, - } - - headers = {"Content-Type" => "application/octet-stream"} - content_server = true - response = @session.do_put path, params, headers, file_obj, content_server - - Dropbox::parse_response(response) + # Returns: + # * A Hash with the metadata of file just uploaded. + # For a detailed description of what this call returns, visit: + # https://www.dropbox.com/developers/reference/api#metadata + def finish(to_path, overwrite=false, parent_rev=nil) + response = @client.commit_chunked_upload(to_path, @upload_id, overwrite, parent_rev) + Dropbox::parse_response(response) end + end - # Returns a ChunkedUploader object. - # - # Args: - # * +file_obj+: The file-like object to be uploaded. Must support .read() - # * +total_size+: The total size of file_obj - def get_chunked_uploader(file_obj, total_size) - ChunkedUploader.new(self, file_obj, total_size) - end + def commit_chunked_upload(to_path, upload_id, overwrite=false, parent_rev=nil) #:nodoc + path = "/commit_chunked_upload/#{@root}#{format_path(to_path)}" + params = {'overwrite' => overwrite.to_s, + 'upload_id' => upload_id, + 'parent_rev' => parent_rev + } + headers = nil + @session.do_post path, params, headers, :content + end - # ChunkedUploader is responsible for uploading a large file to Dropbox in smaller chunks. - # This allows large files to be uploaded and makes allows recovery during failure. - class ChunkedUploader - attr_accessor :file_obj, :total_size, :offset, :upload_id, :client + def partial_chunked_upload(data, upload_id=nil, offset=nil) #:nodoc + params = { + 'upload_id' => upload_id, + 'offset' => offset, + } + headers = {'Content-Type' => "application/octet-stream"} + @session.do_put '/chunked_upload', params, headers, data, :content + end - def initialize(client, file_obj, total_size) - @client = client - @file_obj = file_obj - @total_size = total_size - @upload_id = nil - @offset = 0 - end + # Download a file + # + # Args: + # * +from_path+: The path to the file to be downloaded + # * +rev+: A previous revision value of the file to be downloaded + # + # Returns: + # * The file contents. + def get_file(from_path, rev=nil) + response = get_file_impl(from_path, rev) + Dropbox::parse_response(response, raw=true) + end - # Uploads data from this ChunkedUploader's file_obj in chunks, until - # an error occurs. Throws an exception when an error occurs, and can - # be called again to resume the upload. - # - # Args: - # * +chunk_size+: The chunk size for each individual upload. Defaults to 4MB. - def upload(chunk_size=4*1024*1024) - last_chunk = nil + # Download a file and get its metadata. + # + # Args: + # * +from_path+: The path to the file to be downloaded + # * +rev+: A previous revision value of the file to be downloaded + # + # Returns: + # * The file contents. + # * The file metadata as a hash. + def get_file_and_metadata(from_path, rev=nil) + response = get_file_impl(from_path, rev) + parsed_response = Dropbox::parse_response(response, raw=true) + metadata = parse_metadata(response) + return parsed_response, metadata + end - while @offset < @total_size - if not last_chunk - last_chunk = @file_obj.read(chunk_size) - end + # Download a file (helper method - don't call this directly). + # + # Args: + # * +from_path+: The path to the file to be downloaded + # * +rev+: A previous revision value of the file to be downloaded + # + # Returns: + # * The HTTPResponse for the file download request. + def get_file_impl(from_path, rev=nil) # :nodoc: + path = "/files/#{@root}#{format_path(from_path)}" + params = { + 'rev' => rev, + } + @session.do_get path, params, :content + end + private :get_file_impl - resp = {} - begin - resp = Dropbox::parse_response(@client.partial_chunked_upload(last_chunk, @upload_id, @offset)) - last_chunk = nil - rescue SocketError => e - raise e - rescue SystemCallError => e - raise e - rescue DropboxError => e - raise e if e.http_response.nil? or e.http_response.code[0] == '5' - begin - resp = JSON.parse(e.http_response.body) - raise DropboxError.new('server response does not have offset key') unless resp.has_key? 'offset' - rescue JSON::ParserError - raise DropboxError.new("Unable to parse JSON response: #{e.http_response.body}") - end - end - - if resp.has_key? 'offset' and resp['offset'] > @offset - @offset += (resp['offset'] - @offset) if resp['offset'] - last_chunk = nil - end - @upload_id = resp['upload_id'] if resp['upload_id'] - end - end - - # Completes a file upload - # - # Args: - # * +to_path+: The directory path to upload the file to. If the destination - # directory does not yet exist, it will be created. - # * +overwrite+: Whether to overwrite an existing file at the given path. [default is False] - # If overwrite is False and a file already exists there, Dropbox - # will rename the upload to make sure it doesn't overwrite anything. - # You must check the returned metadata to know what this new name is. - # This field should only be True if your intent is to potentially - # clobber changes to a file that you don't know about. - # * parent_rev: The rev field from the 'parent' of this upload. - # If your intent is to update the file at the given path, you should - # pass the parent_rev parameter set to the rev value from the most recent - # metadata you have of the existing file at that path. If the server - # has a more recent version of the file at the specified path, it will - # automatically rename your uploaded file, spinning off a conflict. - # Using this parameter effectively causes the overwrite parameter to be ignored. - # The file will always be overwritten if you send the most-recent parent_rev, - # and it will never be overwritten you send a less-recent one. - # - # Returns: - # * A Hash with the metadata of file just uploaded. - # For a detailed description of what this call returns, visit: - # https://www.dropbox.com/developers/reference/api#metadata - def finish(to_path, overwrite=false, parent_rev=nil) - response = @client.commit_chunked_upload(to_path, @upload_id, overwrite, parent_rev) - Dropbox::parse_response(response) - end + # Parses out file metadata from a raw dropbox HTTP response. + # + # Args: + # * +dropbox_raw_response+: The raw, unparsed HTTPResponse from Dropbox. + # + # Returns: + # * The metadata of the file as a hash. + def parse_metadata(dropbox_raw_response) # :nodoc: + begin + raw_metadata = dropbox_raw_response['x-dropbox-metadata'] + metadata = JSON.parse(raw_metadata) + rescue + raise DropboxError.new("Dropbox Server Error: x-dropbox-metadata=#{raw_metadata}", + dropbox_raw_response) end + metadata + end + private :parse_metadata - def commit_chunked_upload(to_path, upload_id, overwrite=false, parent_rev=nil) #:nodoc - path = "/commit_chunked_upload/#{@root}#{format_path(to_path)}" - params = {'overwrite' => overwrite.to_s, - 'upload_id' => upload_id, - 'parent_rev' => parent_rev, - } - headers = nil - content_server = true - @session.do_post path, params, headers, content_server - end + # Copy a file or folder to a new location. + # + # Args: + # * +from_path+: The path to the file or folder to be copied. + # * +to_path+: The destination path of the file or folder to be copied. + # This parameter should include the destination filename (e.g. + # from_path: '/test.txt', to_path: '/dir/test.txt'). If there's + # already a file at the to_path, this copy will be renamed to + # be unique. + # + # Returns: + # * A hash with the metadata of the new copy of the file or folder. + # For a detailed description of what this call returns, visit: + # https://www.dropbox.com/developers/reference/api#fileops-copy + def file_copy(from_path, to_path) + params = { + "root" => @root, + "from_path" => format_path(from_path, false), + "to_path" => format_path(to_path, false), + } + response = @session.do_post "/fileops/copy", params + Dropbox::parse_response(response) + end - def partial_chunked_upload(data, upload_id=nil, offset=nil) #:nodoc - params = { - 'upload_id' => upload_id, - 'offset' => offset, - } - headers = {'Content-Type' => "application/octet-stream"} - content_server = true - @session.do_put '/chunked_upload', params, headers, data, content_server - end + # Create a folder. + # + # Arguments: + # * +path+: The path of the new folder. + # + # Returns: + # * A hash with the metadata of the newly created folder. + # For a detailed description of what this call returns, visit: + # https://www.dropbox.com/developers/reference/api#fileops-create-folder + def file_create_folder(path) + params = { + "root" => @root, + "path" => format_path(path, false), + } + response = @session.do_post "/fileops/create_folder", params - # Download a file - # - # Args: - # * +from_path+: The path to the file to be downloaded - # * +rev+: A previous revision value of the file to be downloaded - # - # Returns: - # * The file contents. - def get_file(from_path, rev=nil) - response = get_file_impl(from_path, rev) - Dropbox::parse_response(response, raw=true) - end + Dropbox::parse_response(response) + end - # Download a file and get its metadata. - # - # Args: - # * +from_path+: The path to the file to be downloaded - # * +rev+: A previous revision value of the file to be downloaded - # - # Returns: - # * The file contents. - # * The file metadata as a hash. - def get_file_and_metadata(from_path, rev=nil) - response = get_file_impl(from_path, rev) - parsed_response = Dropbox::parse_response(response, raw=true) - metadata = parse_metadata(response) - return parsed_response, metadata - end + # Deletes a file + # + # Arguments: + # * +path+: The path of the file to delete + # + # Returns: + # * A Hash with the metadata of file just deleted. + # For a detailed description of what this call returns, visit: + # https://www.dropbox.com/developers/reference/api#fileops-delete + def file_delete(path) + params = { + "root" => @root, + "path" => format_path(path, false), + } + response = @session.do_post "/fileops/delete", params + Dropbox::parse_response(response) + end - # Download a file (helper method - don't call this directly). - # - # Args: - # * +from_path+: The path to the file to be downloaded - # * +rev+: A previous revision value of the file to be downloaded - # - # Returns: - # * The HTTPResponse for the file download request. - def get_file_impl(from_path, rev=nil) # :nodoc: - path = "/files/#{@root}#{format_path(from_path)}" - params = { - 'rev' => rev, - } - headers = nil - content_server = true - @session.do_get path, params, headers, content_server - end - private :get_file_impl + # Moves a file + # + # Arguments: + # * +from_path+: The path of the file to be moved + # * +to_path+: The destination path of the file or folder to be moved + # If the file or folder already exists, it will be renamed to be unique. + # + # Returns: + # * A Hash with the metadata of file or folder just moved. + # For a detailed description of what this call returns, visit: + # https://www.dropbox.com/developers/reference/api#fileops-delete + def file_move(from_path, to_path) + params = { + "root" => @root, + "from_path" => format_path(from_path, false), + "to_path" => format_path(to_path, false), + } + response = @session.do_post "/fileops/move", params + Dropbox::parse_response(response) + end - # Parses out file metadata from a raw dropbox HTTP response. - # - # Args: - # * +dropbox_raw_response+: The raw, unparsed HTTPResponse from Dropbox. - # - # Returns: - # * The metadata of the file as a hash. - def parse_metadata(dropbox_raw_response) # :nodoc: - begin - raw_metadata = dropbox_raw_response['x-dropbox-metadata'] - metadata = JSON.parse(raw_metadata) - rescue - raise DropboxError.new("Dropbox Server Error: x-dropbox-metadata=#{raw_metadata}", - dropbox_raw_response) - end - return metadata - end - private :parse_metadata + # Retrives metadata for a file or folder + # + # Arguments: + # * path: The path to the file or folder. + # * list: Whether to list all contained files (only applies when + # path refers to a folder). + # * file_limit: The maximum number of file entries to return within + # a folder. If the number of files in the directory exceeds this + # limit, an exception is raised. The server will return at max + # 25,000 files within a folder. + # * hash: Every directory listing has a hash parameter attached that + # can then be passed back into this function later to save on + # bandwidth. Rather than returning an unchanged folder's contents, if + # the hash matches a DropboxNotModified exception is raised. + # * rev: Optional. The revision of the file to retrieve the metadata for. + # This parameter only applies for files. If omitted, you'll receive + # the most recent revision metadata. + # * include_deleted: Specifies whether to include deleted files in metadata results. + # * include_media_info: Specifies to include media info, such as time_taken for photos + # + # Returns: + # * A Hash object with the metadata of the file or folder (and contained files if + # appropriate). For a detailed description of what this call returns, visit: + # https://www.dropbox.com/developers/reference/api#metadata + def metadata(path, file_limit=25000, list=true, hash=nil, rev=nil, include_deleted=false, include_media_info=false) + params = { + "file_limit" => file_limit.to_s, + "list" => list.to_s, + "include_deleted" => include_deleted.to_s, + "hash" => hash, + "rev" => rev, + "include_media_info" => include_media_info + } - # Copy a file or folder to a new location. - # - # Args: - # * +from_path+: The path to the file or folder to be copied. - # * +to_path+: The destination path of the file or folder to be copied. - # This parameter should include the destination filename (e.g. - # from_path: '/test.txt', to_path: '/dir/test.txt'). If there's - # already a file at the to_path, this copy will be renamed to - # be unique. - # - # Returns: - # * A hash with the metadata of the new copy of the file or folder. - # For a detailed description of what this call returns, visit: - # https://www.dropbox.com/developers/reference/api#fileops-copy - def file_copy(from_path, to_path) - params = { - "root" => @root, - "from_path" => format_path(from_path, false), - "to_path" => format_path(to_path, false), - } - response = @session.do_post "/fileops/copy", params - Dropbox::parse_response(response) + response = @session.do_get "/metadata/#{@root}#{format_path(path)}", params + if response.kind_of? Net::HTTPRedirection + raise DropboxNotModified.new("metadata not modified") end + Dropbox::parse_response(response) + end - # Create a folder. - # - # Arguments: - # * +path+: The path of the new folder. - # - # Returns: - # * A hash with the metadata of the newly created folder. - # For a detailed description of what this call returns, visit: - # https://www.dropbox.com/developers/reference/api#fileops-create-folder - def file_create_folder(path) - params = { - "root" => @root, - "path" => format_path(path, false), - } - response = @session.do_post "/fileops/create_folder", params + # Search directory for filenames matching query + # + # Arguments: + # * path: The directory to search within + # * query: The query to search on (3 character minimum) + # * file_limit: The maximum number of file entries to return/ + # If the number of files exceeds this + # limit, an exception is raised. The server will return at max 1,000 + # * include_deleted: Whether to include deleted files in search results + # + # Returns: + # * A Hash object with a list the metadata of the file or folders matching query + # inside path. For a detailed description of what this call returns, visit: + # https://www.dropbox.com/developers/reference/api#search + def search(path, query, file_limit=1000, include_deleted=false) + params = { + 'query' => query, + 'file_limit' => file_limit.to_s, + 'include_deleted' => include_deleted.to_s + } - Dropbox::parse_response(response) - end + response = @session.do_get "/search/#{@root}#{format_path(path)}", params + Dropbox::parse_response(response) + end - # Deletes a file - # - # Arguments: - # * +path+: The path of the file to delete - # - # Returns: - # * A Hash with the metadata of file just deleted. - # For a detailed description of what this call returns, visit: - # https://www.dropbox.com/developers/reference/api#fileops-delete - def file_delete(path) - params = { - "root" => @root, - "path" => format_path(path, false), - } - response = @session.do_post "/fileops/delete", params - Dropbox::parse_response(response) - end + # Retrive revisions of a file + # + # Arguments: + # * path: The file to fetch revisions for. Note that revisions + # are not available for folders. + # * rev_limit: The maximum number of file entries to return within + # a folder. The server will return at max 1,000 revisions. + # + # Returns: + # * A Hash object with a list of the metadata of the all the revisions of + # all matches files (up to rev_limit entries) + # For a detailed description of what this call returns, visit: + # https://www.dropbox.com/developers/reference/api#revisions + def revisions(path, rev_limit=1000) + params = { + 'rev_limit' => rev_limit.to_s + } - # Moves a file - # - # Arguments: - # * +from_path+: The path of the file to be moved - # * +to_path+: The destination path of the file or folder to be moved - # If the file or folder already exists, it will be renamed to be unique. - # - # Returns: - # * A Hash with the metadata of file or folder just moved. - # For a detailed description of what this call returns, visit: - # https://www.dropbox.com/developers/reference/api#fileops-delete - def file_move(from_path, to_path) - params = { - "root" => @root, - "from_path" => format_path(from_path, false), - "to_path" => format_path(to_path, false), - } - response = @session.do_post "/fileops/move", params - Dropbox::parse_response(response) - end + response = @session.do_get "/revisions/#{@root}#{format_path(path)}", params + Dropbox::parse_response(response) + end - # Retrives metadata for a file or folder - # - # Arguments: - # * path: The path to the file or folder. - # * list: Whether to list all contained files (only applies when - # path refers to a folder). - # * file_limit: The maximum number of file entries to return within - # a folder. If the number of files in the directory exceeds this - # limit, an exception is raised. The server will return at max - # 25,000 files within a folder. - # * hash: Every directory listing has a hash parameter attached that - # can then be passed back into this function later to save on - # bandwidth. Rather than returning an unchanged folder's contents, if - # the hash matches a DropboxNotModified exception is raised. - # * rev: Optional. The revision of the file to retrieve the metadata for. - # This parameter only applies for files. If omitted, you'll receive - # the most recent revision metadata. - # * include_deleted: Specifies whether to include deleted files in metadata results. - # - # Returns: - # * A Hash object with the metadata of the file or folder (and contained files if - # appropriate). For a detailed description of what this call returns, visit: - # https://www.dropbox.com/developers/reference/api#metadata - def metadata(path, file_limit=25000, list=true, hash=nil, rev=nil, include_deleted=false) - params = { - "file_limit" => file_limit.to_s, - "list" => list.to_s, - "include_deleted" => include_deleted.to_s, - "hash" => hash, - "rev" => rev, - } + # Restore a file to a previous revision. + # + # Arguments: + # * path: The file to restore. Note that folders can't be restored. + # * rev: A previous rev value of the file to be restored to. + # + # Returns: + # * A Hash object with a list the metadata of the file or folders restored + # For a detailed description of what this call returns, visit: + # https://www.dropbox.com/developers/reference/api#search + def restore(path, rev) + params = { + 'rev' => rev.to_s + } - response = @session.do_get "/metadata/#{@root}#{format_path(path)}", params - if response.kind_of? Net::HTTPRedirection - raise DropboxNotModified.new("metadata not modified") - end - Dropbox::parse_response(response) - end + response = @session.do_post "/restore/#{@root}#{format_path(path)}", params + Dropbox::parse_response(response) + end - # Search directory for filenames matching query - # - # Arguments: - # * path: The directory to search within - # * query: The query to search on (3 character minimum) - # * file_limit: The maximum number of file entries to return/ - # If the number of files exceeds this - # limit, an exception is raised. The server will return at max 1,000 - # * include_deleted: Whether to include deleted files in search results - # - # Returns: - # * A Hash object with a list the metadata of the file or folders matching query - # inside path. For a detailed description of what this call returns, visit: - # https://www.dropbox.com/developers/reference/api#search - def search(path, query, file_limit=1000, include_deleted=false) - params = { - 'query' => query, - 'file_limit' => file_limit.to_s, - 'include_deleted' => include_deleted.to_s - } + # Returns a direct link to a media file + # All of Dropbox's API methods require OAuth, which may cause problems in + # situations where an application expects to be able to hit a URL multiple times + # (for example, a media player seeking around a video file). This method + # creates a time-limited URL that can be accessed without any authentication. + # + # Arguments: + # * path: The file to stream. + # + # Returns: + # * A Hash object that looks like the following: + # {'url': 'https://dl.dropboxusercontent.com/1/view/abcdefghijk/example', 'expires': 'Thu, 16 Sep 2011 01:01:25 +0000'} + def media(path) + response = @session.do_get "/media/#{@root}#{format_path(path)}" + Dropbox::parse_response(response) + end - response = @session.do_get "/search/#{@root}#{format_path(path)}", params - Dropbox::parse_response(response) - end + # Get a URL to share a media file + # Shareable links created on Dropbox are time-limited, but don't require any + # authentication, so they can be given out freely. The time limit should allow + # at least a day of shareability, though users have the ability to disable + # a link from their account if they like. + # + # Arguments: + # * path: The file to share. + # * short_url: When true (default), the url returned will be shortened using the Dropbox url shortener. If false, + # the url will link directly to the file's preview page. + # + # Returns: + # * A Hash object that looks like the following example: + # {'url': 'https://db.tt/c0mFuu1Y', 'expires': 'Tue, 01 Jan 2030 00:00:00 +0000'} + # For a detailed description of what this call returns, visit: + # https://www.dropbox.com/developers/reference/api#shares + def shares(path, short_url=true) + response = @session.do_get "/shares/#{@root}#{format_path(path)}", {"short_url"=>short_url} + Dropbox::parse_response(response) + end - # Retrive revisions of a file - # - # Arguments: - # * path: The file to fetch revisions for. Note that revisions - # are not available for folders. - # * rev_limit: The maximum number of file entries to return within - # a folder. The server will return at max 1,000 revisions. - # - # Returns: - # * A Hash object with a list of the metadata of the all the revisions of - # all matches files (up to rev_limit entries) - # For a detailed description of what this call returns, visit: - # https://www.dropbox.com/developers/reference/api#revisions - def revisions(path, rev_limit=1000) - params = { - 'rev_limit' => rev_limit.to_s - } + # Download a PDF or HTML preview for a file. + # + # Arguments: + # * path: The path to the file to be previewed. + # * rev: Optional. The revision of the file to retrieve the metadata for. + # If omitted, you'll get the most recent version. + # Returns: + # * The preview data + def preview(path, rev=nil) + path = "/previews/#{@root}#{format_path(path)}" + params = { 'rev' => rev } + response = @session.do_get path, params, :content + Dropbox::parse_response(response, raw=true) + end - response = @session.do_get "/revisions/#{@root}#{format_path(path)}", params - Dropbox::parse_response(response) - end + # Download a thumbnail for an image. + # + # Arguments: + # * from_path: The path to the file to be thumbnailed. + # * size: A string describing the desired thumbnail size. At this time, + # 'small' (32x32), 'medium' (64x64), 'large' (128x128), 's' (64x64), + # 'm' (128x128), 'l' (640x640), and 'xl' (1024x1024) are officially supported sizes. + # Check https://www.dropbox.com/developers/reference/api#thumbnails + # for more details. [defaults to large] + # Returns: + # * The thumbnail data + def thumbnail(from_path, size='large') + response = thumbnail_impl(from_path, size) + Dropbox::parse_response(response, raw=true) + end - # Restore a file to a previous revision. - # - # Arguments: - # * path: The file to restore. Note that folders can't be restored. - # * rev: A previous rev value of the file to be restored to. - # - # Returns: - # * A Hash object with a list the metadata of the file or folders restored - # For a detailed description of what this call returns, visit: - # https://www.dropbox.com/developers/reference/api#search - def restore(path, rev) - params = { - 'rev' => rev.to_s - } + # Download a thumbnail for an image along with the image's metadata. + # + # Arguments: + # * from_path: The path to the file to be thumbnailed. + # * size: A string describing the desired thumbnail size. See thumbnail() + # for details. + # Returns: + # * The thumbnail data + # * The metadata for the image as a hash + def thumbnail_and_metadata(from_path, size='large') + response = thumbnail_impl(from_path, size) + parsed_response = Dropbox::parse_response(response, raw=true) + metadata = parse_metadata(response) + return parsed_response, metadata + end - response = @session.do_post "/restore/#{@root}#{format_path(path)}", params - Dropbox::parse_response(response) - end + # A way of letting you keep a local representation of the Dropbox folder + # heirarchy. You can periodically call delta() to get a list of "delta + # entries", which are instructions on how to update your local state to + # match the server's state. + # + # Arguments: + # * +cursor+: On the first call, omit this argument (or pass in +nil+). On + # subsequent calls, pass in the +cursor+ string returned by the previous + # call. + # * +path_prefix+: If provided, results will be limited to files and folders + # whose paths are equal to or under +path_prefix+. The +path_prefix+ is + # fixed for a given cursor. Whatever +path_prefix+ you use on the first + # +delta()+ must also be passed in on subsequent calls that use the returned + # cursor. + # + # Returns: A hash with three fields. + # * +entries+: A list of "delta entries" (described below) + # * +reset+: If +true+, you should reset local state to be an empty folder + # before processing the list of delta entries. This is only +true+ only + # in rare situations. + # * +cursor+: A string that is used to keep track of your current state. + # On the next call to delta(), pass in this value to return entries + # that were recorded since the cursor was returned. + # * +has_more+: If +true+, then there are more entries available; you can + # call delta() again immediately to retrieve those entries. If +false+, + # then wait at least 5 minutes (preferably longer) before checking again. + # + # Delta Entries: Each entry is a 2-item list of one of following forms: + # * [_path_, _metadata_]: Indicates that there is a file/folder at the given + # path. You should add the entry to your local state. (The _metadata_ + # value is the same as what would be returned by the #metadata() call.) + # * If the path refers to parent folders that don't yet exist in your + # local state, create those parent folders in your local state. You + # will eventually get entries for those parent folders. + # * If the new entry is a file, replace whatever your local state has at + # _path_ with the new entry. + # * If the new entry is a folder, check what your local state has at + # _path_. If it's a file, replace it with the new entry. If it's a + # folder, apply the new _metadata_ to the folder, but do not modify + # the folder's children. + # * [path, +nil+]: Indicates that there is no file/folder at the _path_ on + # Dropbox. To update your local state to match, delete whatever is at + # _path_, including any children (you will sometimes also get separate + # delta entries for each child, but this is not guaranteed). If your + # local state doesn't have anything at _path_, ignore this entry. + # + # Remember: Dropbox treats file names in a case-insensitive but case-preserving + # way. To facilitate this, the _path_ strings above are lower-cased versions of + # the actual path. The _metadata_ dicts have the original, case-preserved path. + def delta(cursor=nil, path_prefix=nil) + params = { + 'cursor' => cursor, + 'path_prefix' => path_prefix, + } - # Returns a direct link to a media file - # All of Dropbox's API methods require OAuth, which may cause problems in - # situations where an application expects to be able to hit a URL multiple times - # (for example, a media player seeking around a video file). This method - # creates a time-limited URL that can be accessed without any authentication. - # - # Arguments: - # * path: The file to stream. - # - # Returns: - # * A Hash object that looks like the following: - # {'url': 'https://dl.dropboxusercontent.com/1/view/abcdefghijk/example', 'expires': 'Thu, 16 Sep 2011 01:01:25 +0000'} - def media(path) - response = @session.do_get "/media/#{@root}#{format_path(path)}" - Dropbox::parse_response(response) - end + response = @session.do_post "/delta", params + Dropbox::parse_response(response) + end - # Get a URL to share a media file - # Shareable links created on Dropbox are time-limited, but don't require any - # authentication, so they can be given out freely. The time limit should allow - # at least a day of shareability, though users have the ability to disable - # a link from their account if they like. - # - # Arguments: - # * path: The file to share. - # - # Returns: - # * A Hash object that looks like the following example: - # {'url': 'https://db.tt/c0mFuu1Y', 'expires': 'Tue, 01 Jan 2030 00:00:00 +0000'} - # For a detailed description of what this call returns, visit: - # https://www.dropbox.com/developers/reference/api#shares - def shares(path) - response = @session.do_get "/shares/#{@root}#{format_path(path)}" - Dropbox::parse_response(response) - end + # Calls the long-poll endpoint which waits for changes on an account. In + # conjunction with #delta, this call gives you a low-latency way to monitor + # an account for file changes. + # + # The passed in cursor can only be acquired via a call to #delta + # + # Arguments: + # * +cursor+: A delta cursor as returned from a call to #delta + # * +timeout+: An optional integer indicating a timeout, in seconds. The + # default value is 30 seconds, which is also the minimum allowed value. The + # maximum is 480 seconds. + # + # Returns: A hash with one or two fields. + # * +changes+: A boolean value indicating whether new changes are available. + # * +backoff+: If present, indicates how many seconds your code should wait + # before calling #longpoll_delta again. + def longpoll_delta(cursor, timeout=30) + params = { + 'cursor' => cursor, + 'timeout' => timeout + } - # Download a thumbnail for an image. - # - # Arguments: - # * from_path: The path to the file to be thumbnailed. - # * size: A string describing the desired thumbnail size. At this time, - # 'small' (32x32), 'medium' (64x64), 'large' (128x128), 's' (64x64), - # 'm' (128x128), 'l' (640x640), and 'xl' (1024x1024) are officially supported sizes. - # Check https://www.dropbox.com/developers/reference/api#thumbnails - # for more details. [defaults to large] - # Returns: - # * The thumbnail data - def thumbnail(from_path, size='large') - response = thumbnail_impl(from_path, size) - Dropbox::parse_response(response, raw=true) - end + response = @session.do_get "/longpoll_delta", params, :notify + Dropbox::parse_response(response) + end - # Download a thumbnail for an image along with the image's metadata. - # - # Arguments: - # * from_path: The path to the file to be thumbnailed. - # * size: A string describing the desired thumbnail size. See thumbnail() - # for details. - # Returns: - # * The thumbnail data - # * The metadata for the image as a hash - def thumbnail_and_metadata(from_path, size='large') - response = thumbnail_impl(from_path, size) - parsed_response = Dropbox::parse_response(response, raw=true) - metadata = parse_metadata(response) - return parsed_response, metadata - end + # Download a thumbnail (helper method - don't call this directly). + # + # Args: + # * +from_path+: The path to the file to be thumbnailed. + # * +size+: A string describing the desired thumbnail size. See thumbnail() + # for details. + # + # Returns: + # * The HTTPResponse for the thumbnail request. + def thumbnail_impl(from_path, size='large') # :nodoc: + path = "/thumbnails/#{@root}#{format_path(from_path, true)}" + params = { + "size" => size + } + @session.do_get path, params, :content + end + private :thumbnail_impl - # A way of letting you keep a local representation of the Dropbox folder - # heirarchy. You can periodically call delta() to get a list of "delta - # entries", which are instructions on how to update your local state to - # match the server's state. - # - # Arguments: - # * +cursor+: On the first call, omit this argument (or pass in +nil+). On - # subsequent calls, pass in the +cursor+ string returned by the previous - # call. - # * +path_prefix+: If provided, results will be limited to files and folders - # whose paths are equal to or under +path_prefix+. The +path_prefix+ is - # fixed for a given cursor. Whatever +path_prefix+ you use on the first - # +delta()+ must also be passed in on subsequent calls that use the returned - # cursor. - # - # Returns: A hash with three fields. - # * +entries+: A list of "delta entries" (described below) - # * +reset+: If +true+, you should reset local state to be an empty folder - # before processing the list of delta entries. This is only +true+ only - # in rare situations. - # * +cursor+: A string that is used to keep track of your current state. - # On the next call to delta(), pass in this value to return entries - # that were recorded since the cursor was returned. - # * +has_more+: If +true+, then there are more entries available; you can - # call delta() again immediately to retrieve those entries. If +false+, - # then wait at least 5 minutes (preferably longer) before checking again. - # - # Delta Entries: Each entry is a 2-item list of one of following forms: - # * [_path_, _metadata_]: Indicates that there is a file/folder at the given - # path. You should add the entry to your local state. (The _metadata_ - # value is the same as what would be returned by the #metadata() call.) - # * If the path refers to parent folders that don't yet exist in your - # local state, create those parent folders in your local state. You - # will eventually get entries for those parent folders. - # * If the new entry is a file, replace whatever your local state has at - # _path_ with the new entry. - # * If the new entry is a folder, check what your local state has at - # _path_. If it's a file, replace it with the new entry. If it's a - # folder, apply the new _metadata_ to the folder, but do not modify - # the folder's children. - # * [path, +nil+]: Indicates that there is no file/folder at the _path_ on - # Dropbox. To update your local state to match, delete whatever is at - # _path_, including any children (you will sometimes also get separate - # delta entries for each child, but this is not guaranteed). If your - # local state doesn't have anything at _path_, ignore this entry. - # - # Remember: Dropbox treats file names in a case-insensitive but case-preserving - # way. To facilitate this, the _path_ strings above are lower-cased versions of - # the actual path. The _metadata_ dicts have the original, case-preserved path. - def delta(cursor=nil, path_prefix=nil) - params = { - 'cursor' => cursor, - 'path_prefix' => path_prefix, - } - response = @session.do_post "/delta", params - Dropbox::parse_response(response) - end + # Creates and returns a copy ref for a specific file. The copy ref can be + # used to instantly copy that file to the Dropbox of another account. + # + # Args: + # * +path+: The path to the file for a copy ref to be created on. + # + # Returns: + # * A Hash object that looks like the following example: + # {"expires"=>"Fri, 31 Jan 2042 21:01:05 +0000", "copy_ref"=>"z1X6ATl6aWtzOGq0c3g5Ng"} + def create_copy_ref(path) + path = "/copy_ref/#{@root}#{format_path(path)}" + response = @session.do_get path + Dropbox::parse_response(response) + end - # Download a thumbnail (helper method - don't call this directly). - # - # Args: - # * +from_path+: The path to the file to be thumbnailed. - # * +size+: A string describing the desired thumbnail size. See thumbnail() - # for details. - # - # Returns: - # * The HTTPResponse for the thumbnail request. - def thumbnail_impl(from_path, size='large') # :nodoc: - path = "/thumbnails/#{@root}#{format_path(from_path, true)}" - params = { - "size" => size - } - headers = nil - content_server = true - @session.do_get path, params, headers, content_server - end - private :thumbnail_impl + # Adds the file referenced by the copy ref to the specified path + # + # Args: + # * +copy_ref+: A copy ref string that was returned from a create_copy_ref call. + # The copy_ref can be created from any other Dropbox account, or from the same account. + # * +to_path+: The path to where the file will be created. + # + # Returns: + # * A hash with the metadata of the new file. + def add_copy_ref(to_path, copy_ref) + params = {'from_copy_ref' => copy_ref, + 'to_path' => "#{to_path}", + 'root' => @root} + response = @session.do_post "/fileops/copy", params + Dropbox::parse_response(response) + end - # Creates and returns a copy ref for a specific file. The copy ref can be - # used to instantly copy that file to the Dropbox of another account. - # - # Args: - # * +path+: The path to the file for a copy ref to be created on. - # - # Returns: - # * A Hash object that looks like the following example: - # {"expires"=>"Fri, 31 Jan 2042 21:01:05 +0000", "copy_ref"=>"z1X6ATl6aWtzOGq0c3g5Ng"} - def create_copy_ref(path) - path = "/copy_ref/#{@root}#{format_path(path)}" - response = @session.do_get path - Dropbox::parse_response(response) - end + #From the oauth spec plus "/". Slash should not be ecsaped + RESERVED_CHARACTERS = /[^a-zA-Z0-9\-\.\_\~\/]/ # :nodoc: - # Adds the file referenced by the copy ref to the specified path - # - # Args: - # * +copy_ref+: A copy ref string that was returned from a create_copy_ref call. - # The copy_ref can be created from any other Dropbox account, or from the same account. - # * +to_path+: The path to where the file will be created. - # - # Returns: - # * A hash with the metadata of the new file. - def add_copy_ref(to_path, copy_ref) - params = {'from_copy_ref' => copy_ref, - 'to_path' => "#{to_path}", - 'root' => @root} + def format_path(path, escape=true) # :nodoc: + path = path.gsub(/\/+/,"/") + # replace multiple slashes with a single one - response = @session.do_post "/fileops/copy", params - Dropbox::parse_response(response) - end + path = path.gsub(/^\/?/,"/") + # ensure the path starts with a slash - #From the oauth spec plus "/". Slash should not be ecsaped - RESERVED_CHARACTERS = /[^a-zA-Z0-9\-\.\_\~\/]/ # :nodoc: + path.gsub(/\/?$/,"") + # ensure the path doesn't end with a slash - def format_path(path, escape=true) # :nodoc: - path = path.gsub(/\/+/,"/") - # replace multiple slashes with a single one - - path = path.gsub(/^\/?/,"/") - # ensure the path starts with a slash - - path.gsub(/\/?$/,"") - # ensure the path doesn't end with a slash - - return URI.escape(path, RESERVED_CHARACTERS) if escape - path - end + return URI.escape(path, RESERVED_CHARACTERS) if escape + path + end end