ファイルアップロード/フォルダ作成/削除に対応
Adobe Shareクライアントに以下の機能を実装。
- ファイルアップロード
- フォルダ作成
- ファイル/フォルダの削除
使い方
require "share-client" # クライアントを作る cl = Share::Client.new( <APIキー>, <shared secret>, <ユーザーID>, <パスワード> ) # セッションを開始 cl.session { |session| # ルートフォルダの情報を得る。 info = session.get_node_info # ルートフォルダのid puts info[:node][:nodeid] # 子要素の情報表示 info[:children].each { |item| puts item[:name] + " : " + item[:nodeid] } # ルートにフォルダを作成 res = session.create_folder( { :name=>"test-folder", :description=>"desc", # optional }, info[:node][:nodeid] ) folder_id = res[:node][:nodeid] # ルートにアップロード session.upload( "./test.zip", { :name=>"test.zip", :description=>"desc", # optional :renditions=>true }, info[:node][:nodeid] ) # 削除 info = session.get_node_info info[:children].each { |item| if item[:name] =~ /^test.*/ session.delete( item[:nodeid] ) # 削除 end } }
ソース
share-client.rb
require 'http-requestor' require 'digest/md5' require "rexml/document" require "multipart-form-data" # Adobe Share クライアント module Share class Base def initialize( api_key, shared_secret ) @api_key = api_key @shared_secret = shared_secret end # 認証ヘッダを挿入する。 def set_authorization_header( req, shared_secret, session_id=nil ) t = Time.new h = req["headers"] data = req["method"] + " " + "https://" + req["host"] + req["path"] data += " " + ( t.to_f * 1000).round.to_s auth = "AdobeAuth " auth << "sessionid=\"" << session_id << "\"," if session_id != nil auth << "apikey=\"" << @api_key << "\"," auth << "data=\"" << data << "\"," auth << "sig=\"" << Digest::MD5.hexdigest( data + shared_secret ) << "\"" h["Authorization"] = auth end # レスポンスを解析する def parse_response( body ) raise body unless body =~ /^\s*<response/ doc = REXML::Document.new(body) res = { :status => doc.attributes["status"], :property => {} } doc.elements.each("./response/*") { |item| res[:property][item.name.to_sym] = item.text } return res end # リクエストを生成する def create_request( hash ) buff = "<request>" buff << _create_request(hash) buff << "</request>" end private def _create_request( hash ) buff = "" hash.each {|k,v| buff << "<#{k.to_s}>" buff << ( v.is_a?(Hash) ? _create_request(v) : v.to_s ) buff << "</#{k.to_s}>" } buff end end class Client < Base def initialize( api_key, shared_secret, user_name, password ) super( api_key, shared_secret ) @user_name = user_name @password = password @hr = HttpRequestor.new( File.dirname(__FILE__) + "/share-requests.yaml") @authtoken = login # ログイン end # セッションを開始する def session( &block ) # セッションを開始 res = @hr.new_session() { |req, conf| set_authorization_header( req, @shared_secret ) req["body"] = create_request( { :authtoken=>@authtoken } ) } res = parse_response( res.body ) session_id = res[:property][:sessionid] secret = res[:property][:secret] begin session = Session.new( @api_key, secret, session_id, @hr ) block.call( session ) if block_given? ensure # セッション終了 if ( session_id != nil ) res = @hr.end_session() { |req, conf| req["path"] = req["path"] + session_id + "/" set_authorization_header( req, secret, session_id ) } parse_response( res.body ) end end end private # ログインする。 def login() res = @hr.auth() { |req, conf| set_authorization_header( req, @shared_secret ) req["body"] = create_request( { :username=>@user_name, :password=>@password } ) } res = parse_response( res.body ) return res[:property][:authtoken] end end class Session < Base def initialize( api_key, shared_secret, session_id, hr ) super( api_key, shared_secret ) @hr = hr @session_id = session_id end # フォルダの情報を取得する。 # @param node_id 情報を取得するノードのID。nilの場合ルートノードの情報を返す def get_node_info( node_id=nil ) res = @hr.get_node_info() { |req, conf| req["path"] = req["path"] + node_id + "/" if node_id != nil set_authorization_header( req, @shared_secret, @session_id ) } return parse_get_nodeinfo_response( res.body ) end # ローカルファイルをアップロードする。 # # file_pathでローカルファイルのパス、 # propertyでフアイルの属性(:nameと:renditionsが必須。)、 # node_idでアップロード先ノードを指定する。(省略するとノードにアップロード) # def upload( file_path, property, node_id=nil ) boundary = "share-client-" << Time.new.to_i.to_s data = MultipartFormData::Data.new boundary data.add( create_request( {:file=>property} ), { "Content-Disposition"=>'form-data; name="request"' } ) data.add_file( file_path, { "Content-Transfer-Encoding"=>"binary", "Content-Type"=>"application/octet-stream", "Content-Disposition"=>'form-data; name="file"; filename="' << URI.encode(property[:name]) << '"' } ) res = @hr.upload() { |req, conf| req["path"] = req["path"] + node_id + "/" if node_id != nil req["headers"]["Content-Type"] = "multipart/form-data; boundary=" << boundary req["headers"]["Content-Length"] = data.size.to_s req["body_stream"] = data set_authorization_header( req, @shared_secret, @session_id ) } return parse_get_nodeinfo_response( res.body ) end # フォルダを作成する def create_folder( property, node_id=nil ) res = @hr.create_folder() { |req, conf| req["path"] = req["path"] + node_id + "/" if node_id != nil req["body"] = create_request( {:folder=>property} ) set_authorization_header( req, @shared_secret, @session_id ) } return parse_get_nodeinfo_response( res.body ) end # フォルダまたはファイルを削除する def delete( node_id=nil ) res = @hr.delete() { |req, conf| req["path"] = req["path"] + node_id + "/" if node_id != nil set_authorization_header( req, @shared_secret, @session_id ) } return parse_response( res.body ) end private # ノード情報取得のレスポンスを解析する def parse_get_nodeinfo_response( body ) raise body unless body =~ /^\s*<response/ doc = REXML::Document.new(body) res = { :status => doc.attributes["status"], :node => {}, :children=> [] } doc.elements.each("./response/node") { |item| item.attributes.each { |k, v| res[:node][k.to_sym] = v } } doc.elements.each("./response/children/node") { |item| attrs = {} item.attributes.each { |k, v| attrs[k.to_sym] = v } res[:children] << attrs } return res end end end
multipart-form-data.rb
昨日作ったもの。変更はしてないはず・・。
require "stringio" module MultipartFormData # multipart/form-dataの送信のため、複数のデータをつなぐIOもどき。 # read(length)のみ実装。 class Data def initialize ( boundary ) @boundary = boundary @datas = [] @index = -1 @io = nil end # IO or 文字列を追加する。 def add( data, headers ) if data.is_a? String src = StringSource.new data elsif data.is_a? IO src = IOSource.new data elsif data.is_a? Source src = data else raise ArgumentError.new end header = "--#{@boundary}\r\n" headers.each {|k,v| header << "#{k.to_s}: #{v.to_s}\r\n" } header << "\r\n" @datas << StringSource.new(header) @datas << src @datas << StringSource.new("\r\n") end # ファイルを追加する。 def add_file( file_path, headers ) add( FileSource.new(file_path), headers) end def read( length=nil ) buff = "" while ( length==nil || buff.length < length ) @io = next_io() if @io == nil return buff.length > 0 ? buff : nil if @io == nil data = @io.read( length==nil ? nil : length - buff.length ) if ( @io.eof? ) @io.close @io = nil end buff += data if data != nil end return buff end # サイズを得る。 def size total = 0 @datas.each { |data| total += data.size } total + @boundary.length + 6 end # リソースを破棄する。 # 不要になった際に、必ず実行すること。 def close begin if (@io != nil) @io.close if !@io.closed? @io = nil end ensure @datas.each { |data| if data.is_a? IO data.close if !data.closed? end } end end private def next_io @index+=1 if ( @index == 0) @datas << StringSource.new("--#{@boundary}--\r\n") end if ( @index < @datas.length ) return @datas[@index].get_io else return nil end end end class Source def get_io; end def size; end end class StringSource < Source def initialize ( data ) @data = data end def get_io StringIO.new(@data) end def size @data.length end end class FileSource < Source def initialize ( file_path ) @file_path = file_path end def get_io File.open(@file_path) end def size File.stat(@file_path).size end end class IOSource < Source def initialize ( data ) @data = data end def get_io @data end def size raise "unsupported operation." end end end
http-requestor.rb
修正。
- POST本文のストリームデータ指定に対応。
- DELETEメソッドに対応
require "net/http" require "net/https" require 'yaml' require 'cgi' class HttpRequestor # 初期化する。 def initialize( request_file="./request.yaml" ) File.open( request_file ) {|f| @request = YAML.load( f ) } end # メソッドが定義されていない場合に呼ばれる。 def method_missing( name, *args, &block ) return do_request( name.to_s, &block ) end # リクエストを実行する。 # ブロックが与えられた場合は、ブロックにリクエスト情報を渡し編集する。 def do_request ( method, &block ) fail "unknown method " << method unless @request["requests"].key?( method ) request = merge( {"params"=>{}, "headers"=>{}}, @request["requests"][method] ) request = merge( request, @request["default"] ) block.call( request, @request["conf"] ) if block_given? response = nil Net::HTTP.version_1_2 requestor = nil if ( @request["conf"] != nil && @request["conf"]["proxy_host"] != nil && @request["conf"]["proxy_port"] != nil ) requestor = Net::HTTP::Proxy( @request["conf"]["proxy_host"], @request["conf"]["proxy_port"] ).new( request["host"], request["port"] ) else requestor = Net::HTTP.new( request["host"], request["port"] ) end # for ssl if ( request["ssl"] ) requestor.use_ssl = true requestor.ca_file = request["ca_file"] requestor.verify_mode = OpenSSL::SSL::VERIFY_PEER requestor.verify_depth = 5 end requestor.start { |http| param = map_to_param( request["params"] ) param += "&" + list_to_param(request["param-list"]) if request["method"] == "GET" request["path"] = request["path"] + "?" + param if param.length > 1 response = http.get( request["path"], request["headers"] ) elsif request["method"] == "POST" req = Net::HTTP::Post.new(request["path"], request["headers"]) if ( request["body_stream"] != nil ) # if a request has body stream, use it. req.body_stream = request["body_stream"] else req.body = request["body"] != nil ? request["body"] : param end response = http.request(req) elsif request["method"] == "DELETE" req = Net::HTTP::Delete.new(request["path"], request["headers"]) response = http.request(req) end } return response end private def map_to_param( map ) return "" if map == nil str = "" map.each { | key, value | str << key << '=' << CGI.escape(value.to_s) << '&' if value != nil } return str end def list_to_param( list ) return "" if list == nil str = "" list.each { | item | if item["key"] != nil && item["value"] != nil str << item["key"] << '=' << CGI.escape(item["value"].to_s) << '&' end } return str end # リクエストの設定をマージする。 def merge (request, parent) return request if ( parent == nil ) request.merge!(parent) { |key, self_val, other_val| if ( key == "headers" || key == "params" ) merge(self_val != nil ? self_val : {}, other_val) else self_val != nil ? self_val : other_val end } return request end end
接続先の設定ファイル(share-requests.yaml)
--- conf: #proxy_host: <プロキシホスト> #proxy_port: <プロキシポート> default: host: api.share.adobe.com path: /webservices/api/v1/ port: 443 ssl: true ca_file: "./v.cer" # サーバーの公開鍵証明書 headers: requests: auth: path: /webservices/api/v1/auth/ method: POST new_session: path: /webservices/api/v1/sessions/ method: POST end_session: path: /webservices/api/v1/sessions/ method: DELETE get_node_info: path: /webservices/api/v1/dc/ method: GET upload: path: /webservices/api/v1/dc/ method: POST create_folder: path: /webservices/api/v1/dc/ method: POST delete: path: /webservices/api/v1/dc/ method: DELETE