無料で使えるシステムトレードフレームワーク「Jiji」 をリリースしました!

・OANDA Trade APIを利用した、オープンソースのシステムトレードフレームワークです。
・自分だけの取引アルゴリズムで、誰でも、いますぐ、かんたんに、自動取引を開始できます。

ファイルアップロード/フォルダ作成/削除に対応

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