module Dbox NUM_TRIES = 3 TIME_BETWEEN_TRIES = 3 # in seconds class ConfigurationError < RuntimeError; end class ServerError < RuntimeError; end class RemoteMissing < RuntimeError; end class RemoteAlreadyExists < RuntimeError; end class RequestDenied < RuntimeError; end class API include Loggable def self.authorize app_key = ENV["DROPBOX_APP_KEY"] app_secret = ENV["DROPBOX_APP_SECRET"] raise(ConfigurationError, "Please set the DROPBOX_APP_KEY environment variable to a Dropbox application key") unless app_key raise(ConfigurationError, "Please set the DROPBOX_APP_SECRET environment variable to a Dropbox application secret") unless app_secret flow = DropboxOAuth2FlowNoRedirect.new(app_key, app_secret) authorize_url = flow.start() puts '1. Go to: ' + authorize_url puts '2. Click "Allow" (you might have to log in first)' puts '3. Copy the authorization code' print 'Enter the authorization code here: ' code = STDIN.readline.strip # This will fail if the user gave us an invalid authorization code access_token, user_id = flow.finish(code) puts "export DROPBOX_ACCESS_TOKEN=#{access_token}" puts "export DROPBOX_USER_ID=#{user_id}" puts puts "This auth token will last for 10 years, or when you choose to invalidate it, whichever comes first." puts puts "Now either include these constants in yours calls to dbox, or set them as environment variables." puts "In bash, including them in calls looks like:" puts "$ DROPBOX_ACCESS_TOKEN=#{access_token} DROPBOX_USER_ID=#{user_id} dbox ..." end def self.connect api = new() api.connect api end attr_reader :client # IMPORTANT: API.new is private. Please use API.authorize or API.connect as the entry point. private_class_method :new def initialize end def initialize_copy(other) @client = other.client.clone() end def connect access_token = ENV["DROPBOX_ACCESS_TOKEN"] access_type = ENV["DROPBOX_ACCESS_TYPE"] || "dropbox" raise(ConfigurationError, "Please set the DROPBOX_ACCESS_TOKEN environment variable to a Dropbox access token") unless access_token raise(ConfigurationError, "Please set the DROPBOX_ACCESS_TYPE environment variable either dropbox (full access) or sandbox (App access)") unless access_type == "dropbox" || access_type == "app_folder" @client = DropboxClient.new(access_token) end def run(path, tries = NUM_TRIES, &proc) begin res = proc.call handle_response(path, res) { raise RuntimeError, "Unexpected result: #{res.inspect}" } rescue DropboxNotModified => e :not_modified rescue DropboxAuthError => e raise e rescue DropboxError => e if tries > 0 if e.http_response.kind_of?(Net::HTTPServiceUnavailable) log.info "Encountered 503 on #{path} (likely rate limiting). Sleeping #{TIME_BETWEEN_TRIES}s and trying again." # TODO check for "Retry-After" header and use that for sleep instead of TIME_BETWEEN_TRIES log.info "Headers: #{e.http_response.to_hash.inspect}" else log.info "Encountered a dropbox error. Sleeping #{TIME_BETWEEN_TRIES}s and trying again. Error: #{e.inspect}" log.info "Headers: #{e.http_response.to_hash.inspect}" end sleep TIME_BETWEEN_TRIES run(path, tries - 1, &proc) else handle_response(path, e.http_response) { raise ServerError, "Server error -- might be a hiccup, please try your request again (#{e.message})" } end rescue Exception => e if tries > 0 log.info "Encounted an unknown error. Sleeping #{TIME_BETWEEN_TRIES}s and trying again. Error: #{e.inspect}" sleep TIME_BETWEEN_TRIES run(path, tries - 1, &proc) else raise e end end end def handle_response(path, res, &else_proc) case res when Hash InsensitiveHash[res] when String res when Net::HTTPNotFound raise RemoteMissing, "#{path} does not exist on Dropbox" when Net::HTTPForbidden raise RequestDenied, "Operation on #{path} denied" when Net::HTTPNotModified :not_modified when true true else else_proc.call() end end def metadata(path = "/", hash = nil, list=true) run(path) do log.debug "Fetching metadata for #{path}" res = @client.metadata(path, 10000, list, hash) log.debug res.inspect raise Dbox::RemoteMissing, "#{path} has been deleted on Dropbox" if res["is_deleted"] res end end def create_dir(path) run(path) do log.info "Creating #{path}" begin @client.file_create_folder(path) rescue DropboxError => e if e.http_response.kind_of?(Net::HTTPForbidden) raise RemoteAlreadyExists, "Either the directory at #{path} already exists, or it has invalid characters in the name" else raise e end end end end def delete_dir(path) run(path) do log.info "Deleting #{path}" @client.file_delete(path) end end def get_file(path, file_obj, stream=false) unless stream # just download directly using the get_file API res = run(path) do log.info "Downloading #{path}" @client.get_file(path) end if res.kind_of?(String) file_obj << res true else raise DropboxError.new("Invalid response #{res.inspect}") end else # use the media API to get a URL that we can stream from, and # then stream the file to disk res = run(path) { @client.media(path) } url = res[:url] if res && res.kind_of?(Hash) if url log.info "Downloading #{path}" streaming_download(url, file_obj) else get_file(path, file_obj, false) end end end def put_file(path, local_path, previous_revision=nil) run(path) do log.info "Uploading #{path}" File.open(local_path, "r") {|f| @client.put_file(path, f, false, previous_revision) } end end def delete_file(path) run(path) do log.info "Deleting #{path}" @client.file_delete(path) end end def move(old_path, new_path) run(old_path) do log.info "Moving #{old_path} to #{new_path}" begin @client.file_move(old_path, new_path) rescue DropboxError => e if e.http_response.kind_of?(Net::HTTPForbidden) raise RemoteAlreadyExists, "Error during move -- there may already be a Dropbox folder at #{new_path}" else raise e end end end end def streaming_download(url, io, num_redirects = 0) url = URI.parse(url) http = Net::HTTP.new(url.host, url.port) http.use_ssl = true http.verify_mode = OpenSSL::SSL::VERIFY_NONE http.ca_file = Dropbox::TRUSTED_CERT_FILE req = Net::HTTP::Get.new(url.request_uri) req["User-Agent"] = "OfficialDropboxRubySDK/#{Dropbox::SDK_VERSION}" http.request(req) do |res| if res.kind_of?(Net::HTTPSuccess) # stream into given io res.read_body {|chunk| io.write(chunk) } true else if res.kind_of?(Net::HTTPRedirection) && res.header['location'] && num_redirects < 10 log.info("following redirect, num_redirects = #{num_redirects}") log.info("redirect url: #{res.header['location']}") streaming_download(res.header['location'], io, num_redirects + 1) else raise DropboxError.new("Invalid response #{res.inspect}") end end end end end end