lib/dropbox_sdk.rb in dropbox-sdk-1.0.beta vs lib/dropbox_sdk.rb in dropbox-sdk-1.1
- old
+ new
@@ -1,137 +1,220 @@
require 'rubygems'
-require 'oauth'
-require 'json'
require 'uri'
+require 'net/https'
+require 'cgi'
+require 'json'
require 'yaml'
-DROPBOX_API_SERVER = "api.dropbox.com"
-DROPBOX_API_CONTENT_SERVER = "api-content.dropbox.com"
+module Dropbox
+ API_SERVER = "api.dropbox.com"
+ API_CONTENT_SERVER = "api-content.dropbox.com"
+ WEB_SERVER = "www.dropbox.com"
-API_VERSION = 1
+ API_VERSION = 1
+ SDK_VERSION = "1.1"
+end
# DropboxSession is responsible for holding OAuth 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
- def initialize(key, secret)
- @consumer_key = key
- @consumer_secret = secret
- @oauth_conf = {
- :site => "https://" + DROPBOX_API_SERVER,
- :scheme => :header,
- :http_method => :post,
- :request_token_url => "/#{API_VERSION}/oauth/request_token",
- :access_token_url => "/#{API_VERSION}/oauth/access_token",
- :authorize_url => "/#{API_VERSION}/oauth/authorize",
- }
- @consumer = OAuth::Consumer.new(@consumer_key, @consumer_secret, @oauth_conf)
- @access_token = nil
+ def initialize(consumer_key, consumer_secret)
+ @consumer_key = consumer_key
+ @consumer_secret = consumer_secret
@request_token = nil
+ @access_token = nil
end
- # This gets a request token. Callbacks are excluded, and any arguments provided are passed on
- # to the oauth gem's get_request_token call.
- def get_request_token(*args)
- begin
- @request_token ||= @consumer.get_request_token({:exclude_callback => true}, *args)
- rescue OAuth::Unauthorized => e
- raise DropboxAuthError.new("Could not get request token, unauthorized. Is your app key and secret correct? #{e}")
+ private
+
+ def do_http(uri, auth_token, request) # :nodoc:
+ http = Net::HTTP.new(uri.host, uri.port)
+ http.use_ssl = true
+
+ request.add_field('Authorization', build_auth_header(auth_token))
+
+ #We use this to better understand how developers are using our SDKs.
+ request['User-Agent'] = "OfficialDropboxRubySDK/#{Dropbox::SDK_VERSION}"
+
+ http.request(request)
+ end
+
+ 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)
+ do_http(uri, token, Net::HTTP::Get.new(uri.request_uri))
+ end
+
+ public
+
+ def do_get(url, headers=nil) # :nodoc:
+ assert_authorized
+ do_get_with_token(url, @access_token)
+ end
+
+ def do_http_with_body(uri, request, body)
+ if body != nil
+ if body.is_a?(Hash)
+ form_data = {}
+ body.each {|k,v| form_data[k.to_s] = v if !v.nil?}
+ request.set_form_data(form_data)
+ 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
+ end
+ do_http(uri, @access_token, request)
+ end
+
+ def do_post(url, headers=nil, body=nil) # :nodoc:
+ assert_authorized
+ uri = URI.parse(url)
+ do_http_with_body(uri, Net::HTTP::Post.new(uri.request_uri, headers), body)
+ end
+
+ def do_put(url, headers=nil, body=nil) # :nodoc:
+ assert_authorized
+ uri = URI.parse(url)
+ do_http_with_body(uri, Net::HTTP::Put.new(uri.request_uri, headers), body)
+ end
+
+
+ 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])
+ 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
+
# This returns a URL that your user must visit to grant
# permissions to this application.
- def get_authorize_url(callback=nil, *args)
- get_request_token(*args)
+ def get_authorize_url(callback=nil)
+ get_request_token()
- url = @request_token.authorize_url
+ url = "/#{Dropbox::API_VERSION}/oauth/authorize?oauth_token=#{URI.escape(@request_token.key)}"
if callback
- url += "&oauth_callback=" + URI.escape(callback)
+ url += "&oauth_callback=#{URI.escape(callback)}"
end
+ if @locale
+ url += "&locale=#{URI.escape(@locale)}"
+ end
- "https://www.dropbox.com" + url
+ "https://#{Dropbox::WEB_SERVER}#{url}"
end
# Clears the access_token
def clear_access_token
@access_token = nil
end
+ # Returns the request token, or nil if one hasn't been acquired yet.
+ def request_token
+ @request_token
+ end
+
+ # Returns the access token, or nil if one hasn't been acquired yet.
+ def access_token
+ @access_token
+ 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 = OAuth::RequestToken.new(@consumer, key, secret)
+ @request_token = OAuthToken.new(key, secret)
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 = OAuth::AccessToken.from_hash(@consumer, {:oauth_token => key, :oauth_token_secret => secret})
+ @access_token = OAuthToken.new(key, secret)
end
- def check_authorized
- ##this check is applied before the token and secret methods
- raise DropboxError.new('Session does not yet have a request token') unless authorized?
- end
-
- # Returns the current oauth access or request token.
- def token
- (@access_token || @request_token).token
- end
-
- # Returns the current oauth access or request token secret.
- def secret
- (@access_token || @request_token).secret
- 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 got to the authorize_url and approved your request
+ # your user has gone to the authorize_url and approved your request
def get_access_token
- if @access_token.nil?
- if @request_token.nil?
- raise DropboxAuthError.new("No request token. You must set this or get an authorize url first.")
- end
+ return @access_token if authorized?
- begin
- @access_token = @request_token.get_access_token
- rescue OAuth::Unauthorized => e
- raise DropboxAuthError.new("Could not get access token, unauthorized. Did you go to authorize_url? #{e}")
- end
+ if @request_token.nil?
+ raise DropboxAuthError.new("No request token. You must set this or get an authorize url first.")
end
- @access_token
+
+ @access_token = get_token("/access_token", @request_token, "Couldn't get access token.")
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 true if this Session has been authorized and has an access_token.
def authorized?
!!@access_token
end
# serialize the DropboxSession.
# At DropboxSession's state is capture in three key/secret pairs. Consumer, request, and access.
- # This takes the form of an array that is then converted to yaml
+ # 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.token
+ toreturn.push @access_token.secret, @access_token.key
end
- get_request_token unless @request_token
+ get_request_token
- toreturn.push @request_token.secret, @request_token.token
+ toreturn.push @request_token.secret, @request_token.key
toreturn.push @consumer_secret, @consumer_key
toreturn.to_yaml
end
- # Takes a serialized DropboxSession and returns a new DropboxSession object
+ # 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)
@@ -141,11 +224,27 @@
session
end
end
+# A class that represents either an OAuth request token or an OAuth access token.
+class OAuthToken
+ def initialize(key, secret)
+ @key = key
+ @secret = secret
+ end
+ def key
+ @key
+ end
+
+ def 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
@@ -177,36 +276,25 @@
class DropboxClient
# Initialize a new DropboxClient. You need to get it a session which either has been authorized. See
# documentation on DropboxSession for how to authorize it.
def initialize(session, root="app_folder", locale=nil)
- if not session.authorized?
- begin
- ## attempt to get an access token and authorize the session
- session.get_access_token
- rescue OAuth::Unauthorized => e
- raise DropboxAuthError.new("Could not initialize. Failed to get access token from Session. Error was: #{ e.message }")
- # If this was raised, the user probably didn't go to auth.get_authorize_url
- end
- end
+ session.get_access_token
@root = root.to_s # If they passed in a symbol, make it a string
if not ["dropbox","app_folder"].include?(@root)
raise DropboxError.new("root must be :dropbox or :app_folder")
end
if @root == "app_folder"
#App Folder is the name of the access type, but for historical reasons
- #sandbox is the URL root compontent that indicates this
+ #sandbox is the URL root component that indicates this
@root = "sandbox"
end
@locale = locale
@session = session
- @token = session.get_access_token
-
- #There's no gurantee that @token is still valid, so be sure to handle any DropboxAuthErrors that can be raised
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 parse_response(response, raw=false) # :nodoc:
@@ -234,29 +322,28 @@
begin
return JSON.parse(response.body)
rescue JSON::ParserError
raise DropboxError.new("Unable to parse JSON response", response)
end
-
end
# Returns account info in a Hash object
#
# For a detailed description of what this call returns, visit:
# https://www.dropbox.com/developers/docs#account-info
def account_info()
- response = @token.get build_url("/account/info")
+ response = @session.do_get build_url("/account/info")
parse_response(response)
end
# Uploads a file to a server. This uses the HTTP PUT upload method for simplicity
#
# Arguments:
# * 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
+ # * 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.
@@ -273,29 +360,28 @@
# 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(session, "app_folder")
+ # client = DropboxClient(session, :app_folder)
# #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
}
params['parent_rev'] = parent_rev unless parent_rev.nil?
- response = @token.put(build_url(path, params, content_server=true),
- file_obj,
- "Content-Type" => "application/octet-stream")
+ response = @session.do_put(build_url(path, params, content_server=true),
+ {"Content-Type" => "application/octet-stream"},
+ file_obj)
parse_response(response)
end
# Download a file
@@ -309,11 +395,11 @@
def get_file(from_path, rev=nil)
params = {}
params['rev'] = rev.to_s if rev
path = "/files/#{@root}#{format_path(from_path)}"
- response = @token.get(build_url(path, params, content_server=true))
+ response = @session.do_get build_url(path, params, content_server=true)
parse_response(response, raw=true)
end
# Copy a file or folder to a new location.
@@ -334,11 +420,11 @@
params = {
"root" => @root,
"from_path" => format_path(from_path, false),
"to_path" => format_path(to_path, false),
}
- response = @token.get(build_url("/fileops/copy", params))
+ response = @session.do_post build_url("/fileops/copy", params)
parse_response(response)
end
# Create a folder.
#
@@ -350,13 +436,13 @@
# For a detailed description of what this call returns, visit:
# https://www.dropbox.com/developers/docs#fileops-create-folder
def file_create_folder(path)
params = {
"root" => @root,
- "path" => format_path(from_path, false),
+ "path" => format_path(path, false),
}
- response = @token.get(build_url("/fileops/create_folder", params))
+ response = @session.do_post build_url("/fileops/create_folder", params)
parse_response(response)
end
# Deletes a file
@@ -369,13 +455,13 @@
# For a detailed description of what this call returns, visit:
# https://www.dropbox.com/developers/docs#fileops-delete
def file_delete(path)
params = {
"root" => @root,
- "path" => format_path(from_path, false),
+ "path" => format_path(path, false),
}
- response = @token.get(build_url("/fileops/delete", params))
+ response = @session.do_post build_url("/fileops/delete", params)
parse_response(response)
end
# Moves a file
#
@@ -392,11 +478,11 @@
params = {
"root" => @root,
"from_path" => format_path(from_path, false),
"to_path" => format_path(to_path, false),
}
- response = @token.post(build_url("/fileops/move", params))
+ response = @session.do_post build_url("/fileops/move", params)
parse_response(response)
end
# Retrives metadata for a file or folder
#
@@ -423,11 +509,11 @@
"list" => list.to_s
}
params["hash"] = hash if hash
- response = @token.get build_url("/metadata/#{@root}#{format_path(path)}", params=params)
+ response = @session.do_get build_url("/metadata/#{@root}#{format_path(path)}", params=params)
if response.kind_of? Net::HTTPRedirection
raise DropboxNotModified.new("metadata not modified")
end
parse_response(response)
end
@@ -445,20 +531,18 @@
# 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/docs#search
def search(path, query, file_limit=10000, include_deleted=false)
-
params = {
'query' => query,
'file_limit' => file_limit.to_s,
'include_deleted' => include_deleted.to_s
}
- response = @token.get(build_url("/search/#{@root}#{format_path(path)}", params))
+ response = @session.do_get build_url("/search/#{@root}#{format_path(path)}", params)
parse_response(response)
-
end
# Retrive revisions of a file
#
# Arguments:
@@ -476,11 +560,11 @@
params = {
'rev_limit' => rev_limit.to_s
}
- response = @token.get(build_url("/revisions/#{@root}#{format_path(path)}", params))
+ response = @session.do_get build_url("/revisions/#{@root}#{format_path(path)}", params)
parse_response(response)
end
# Restore a file to a previous revision.
@@ -496,11 +580,11 @@
def restore(path, rev)
params = {
'rev' => rev.to_s
}
- response = @token.get(build_url("/restore/#{@root}#{format_path(path)}", params))
+ response = @session.do_post build_url("/restore/#{@root}#{format_path(path)}", params)
parse_response(response)
end
# Returns a direct link to a media file
# All of Dropbox's API methods require OAuth, which may cause problems in
@@ -513,11 +597,11 @@
#
# Returns:
# * A Hash object that looks like the following:
# {'url': 'https://dl.dropbox.com/0/view/wvxv1fw6on24qw7/file.mov', 'expires': 'Thu, 16 Sep 2011 01:01:25 +0000'}
def media(path)
- response = @token.get(build_url("/media/#{@root}#{format_path(path)}"))
+ response = @session.do_get build_url("/media/#{@root}#{format_path(path)}")
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
@@ -532,11 +616,11 @@
# * A Hash object that looks like the following example:
# {'url': 'http://www.dropbox.com/s/m/a2mbDa2', 'expires': 'Thu, 16 Sep 2011 01:01:25 +0000'}
# For a detailed description of what this call returns, visit:
# https://www.dropbox.com/developers/docs#share
def shares(path)
- response = @token.get(build_url("/shares/#{@root}#{format_path(path)}"))
+ response = @session.do_get build_url("/shares/#{@root}#{format_path(path)}")
parse_response(response)
end
# Download a thumbnail for an image.
#
@@ -558,18 +642,18 @@
"size" => size
}
url = build_url("/thumbnails/#{@root}#{from_path}", params, content_server=true)
- response = @token.get(url)
+ response = @session.do_get url
parse_response(response, raw=true)
end
def build_url(url, params=nil, content_server=false) # :nodoc:
port = 443
- host = content_server ? DROPBOX_API_CONTENT_SERVER : DROPBOX_API_SERVER
- versioned_url = "/#{API_VERSION}#{url}"
+ host = content_server ? Dropbox::API_CONTENT_SERVER : Dropbox::API_SERVER
+ versioned_url = "/#{Dropbox::API_VERSION}#{url}"
target = URI::Generic.new("https", nil, host, port, nil, versioned_url, nil, nil, nil)
#add a locale param if we have one
#initialize a params object is we don't have one
@@ -583,25 +667,24 @@
}.join("&")
end
target.to_s
end
-end
+ #From the oauth spec plus "/". Slash should not be ecsaped
+ RESERVED_CHARACTERS = /[^a-zA-Z0-9\-\.\_\~\/]/
-#From the oauth spec plus "/". Slash should not be ecsaped
-RESERVED_CHARACTERS = /[^a-zA-Z0-9\-\.\_\~\/]/
+ def format_path(path, escape=true) # :nodoc:
+ path = path.gsub(/\/+/,"/")
+ # replace multiple slashes with a single one
-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 = path.gsub(/^\/?/,"/")
- # ensure the path starts with a slash
+ path.gsub(/\/?$/,"")
+ # ensure the path doesn't end 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
-