module Pione module Location # DropboxLocation represents locations on Dropbox server. class DropboxLocation < DataLocation set_scheme "dropbox" define(:need_caching, true) define(:real_appendable, false) define(:writable, true) class << self attr_reader :client # Return true if Dropbox's access token cache exists. # # @return [Boolean] # true if Dropbox's access token cache exists def cached? cache = Pathname.new("~/.pione/dropbox_api.cache").expand_path return (cache.exist? and cache.read.size > 0) end # Setup Dropbox location for CUI client. This method gets Dropbox's # access token from cache file or OAuth2. # # @param tuple_space [TupleSpaceServer] # tuple space # @return [void] def setup_for_cui_client(tuple_space) return if @client access_token = nil cache = Pathname.new("~/.pione/dropbox_api.cache").expand_path if cache.exist? # load access token from cache file access_token = cache.read else # get access token by OAtuh2 setup_consumer_key_and_secret flow = auth_by_oauth2flow_no_redirect access_token, user_id = get_code_for_cui_client(flow) # cache session cache.open("w+") {|c| c.write access_token} end # make a client @client = DropboxClient.new(access_token) # share access token in tuple space share_access_token(tuple_space, access_token) end # Enable Dropbox locations. This method get an access token from the # tuple space and make a Dropbox client. This assumes to be called from # task worker agents. # # @param tuple_space_server [TupleSpaceServer] # tuple space server # @return [void] def enable(tuple_space) tuple = TupleSpace::AttributeTuple.new("dropbox_access_token", nil) if tuple_access_token = tuple_space.read!(tuple) @client = DropboxClient.new(tuple_access_token.value) else raise DropboxLocationUnavailable.new("There is no access token.") end end def rebuild(path) Location["dropbox:%s" % path] end # Setup Dropbox's consumer key and secret. They are loaded from a YAML # file "dropbox_api.yml" at PIONE's home directory. # # @return [void] def setup_consumer_key_and_secret path = Pathname.new("~/.pione/dropbox_api.yml").expand_path if File.exist?(path) api = YAML.load(path.read) @consumer_key = api["key"] @consumer_secret = api["secret"] else raise DropboxLocationUnavailable.new("There are no consumer key and consumer secret.") end end # Authorize dropbox account by `DropboxOAuth2FlowNoRedirect`. # # @return [String] # authorize URL def auth_by_oauth2flow_no_redirect if @consumer_key and @consumer_secret return DropboxOAuth2FlowNoRedirect.new(@consumer_key, @consumer_secret) else raise DropboxLocationUnavailable.new("There are no consumer key and consumer secret.") end end # Authorize dropbox account by `DropboxOAuth2Flow`. # # @param redirect [String] # redirect URL # @param session [Hash] # session # @return [String] # authorize URL def auth_by_oauth2flow(redirect, session) if @consumer_key and @consumer_secret return DropboxOAuth2Flow.new(@consumer_key, @consumer_secret, redirect, session, :dropbox_auth_csrf_token) else raise DropboxLocationUnavailabel.new("There are no consumer key and consumer secret.") end end # Share dropbox's access token with PIONE agents. # # @param tuple_space_server [TupleSpaceServer] # tuple space server # @param access_token [String] # access token # @return [void] def share_access_token(tuple_space_server, access_token) tuple = TupleSpace::AttributeTuple.new("dropbox_access_token", access_token) tuple_space_server.write(tuple) end # Get code for CUI client. # # @return [Array] # access token and Dropbox user ID def get_code_for_cui_client(flow) puts '1. Go to: %s' % flow.start puts '2. Click "Allow"' puts '3. Copy the authorization code' print 'Enter the authorization code here: ' code = STDIN.gets.strip flow.finish(code) end end def initialize(uri) super(uri) end def create(data) if exist? raise ExistAlready.new(self) else client.put_file(@path.to_s, StringIO.new(data)) end return self end def read exist? ? client.get_file(@path.to_s) : (raise NotFound.new(self)) end def update(data) client.put_file(@path.to_s, StringIO.new(data), true) return self end def delete if exist? client.file_delete(@path.to_s) end end # dropbox have "modified" time only, therefore ctime is not implemented def mtime metadata = client.metadata(@path.to_s) Time.parse(metadata["modified"]) end def size metadata = client.metadata(@path.to_s) return metadata["bytes"] end def entries(option={}) rel_entries(option).map {|entry| rebuild(@path + entry)} end def rel_entries(option={}) list = [] raise NotFound.new(self) if not(directory?) metadata = client.metadata(@path.to_s) metadata["contents"].select{|entry| not(entry["is_deleted"])}.each do |entry| list << entry["path"].sub(@path.to_s, "").sub(/^\//, "") entry_location = rebuild(@path + File.basename(entry["path"])) if option[:rec] and entry_location.directory? _list = entry_location.rel_entries(option).map {|subentry| entry + subentry} list = list + _list end end return list end def exist? metadata = client.metadata(@path.to_s) return not(metadata["is_deleted"]) rescue DropboxLocationUnavailable raise rescue DropboxError return false end def file? begin metadata = client.metadata(@path.to_s) return (not(metadata["is_dir"]) and not(metadata["is_deleted"])) rescue DropboxError # when there exists no files and no directories false end end def directory? begin metadata = client.metadata(@path.to_s) return (metadata["is_dir"] and not(metadata["is_deleted"])) rescue DropboxError # when there exists no files and no directories false end end def mkdir unless exist? client.file_create_folder(@path.to_s) end end def move(dest) if dest.scheme == scheme client.file_move(@path.to_s, dest.path) else copy(dest) delete end end def copy(dest) raise NotFound.new(self) unless exist? if dest.scheme == scheme client.file_copy(@path.to_s, dest.path) else dest.write(read) end end def link(orig) if orig.scheme == scheme orig.copy(link) else update(orig.read) end end def turn(dest) copy(dest) end private # Check availability of Dropbox's access token. # # @return [void] def client if self.class.client.nil? # raise an exception when Dropbox client isn't enabled raise DropboxLocationUnavailable.new("There is no access token.") else self.class.client end end end end end