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

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

複数のデータを連結するIOもどき( multipart/form-dataの送信用 )

multipart/form-dataの送信のため、複数のデータ(ファイルパス、IO、文字列のいずれか)を指定したboundaryで連結するIOもどきを作った。IOAPIを一式実装しようかと思ったけど多すぎるので速やかに挫折。Net::HTTPで使えればいいのでread(length)だけ実装した。

使い方

data = nil
begin
  data = MultipartFormData.new "test"

  # 文字列データを追加。
  data.add( "aaaa", {
    "content-disposition"=>'form-data; name="pics"; filename="file1.txt"',
    "Content-Type"=>"text/plain"
  } )
  data.add( "xxxx", {
    "Content-Type"=>"text/plain"
  } )
  # IOを追加。("./data.dat"の中身は「hogehoge」)
  data.add( File.open("./data.dat"), {
    "Content-Type"=>"application/octet-stream"
  } )
  # ローカルファイルを追加。
  data.add_file( "./data.dat", {
    "Content-Type"=>"application/octet-stream"
  } )

  # 読み込み # 読み込んだ後、再度読むことはできないので注意
  puts data.read
  
  #while ( str = data.read(100) )
  #  puts str
  #end

ensure
  data.close if data!= nil # 必ず実行すること。
end

実行結果です。

--test
Content-Type: text/plain
content-disposition: form-data; name="pics"; filename="file1.txt"


aaaa
--test
Content-Type: text/plain

xxxx
--test
Content-Type: application/octet-stream


hogehoge
--test
Content-Type: application/octet-stream

hogehoge
--test--

実装は以下。連結処理の内容は次の通りです。

  1. データの区切り(「"--" + boundary」)を出力
  2. 各データのヘッダを「\r\n」区切りで出力。
  3. データを出力
  4. データの分だけ1-3を繰り返す。
  5. 最後に「"--" + boundary + "--"」を付ける
require "stringio"

# multipart/form-dataの送信のため、複数のデータをつなぐIOもどき。
# read(length)のみ実装。
class MultipartFormData

  def initialize ( boundary )
    @boundary = boundary
    @datas = []
    @index = -1
    @io = nil
  end

  # IO or 文字列を追加する。
  def add( data, headers )
    raise ArgumentError.new  if data == nil || ( !(data.is_a?(IO)) && !(data.is_a?(String)) )
    @datas <<  "--#{@boundary}\r\n"
    headers.each {|k,v|
      @datas << "#{k.to_s}: #{v.to_s}\r\n"
    }
    @datas << "\r\n"
    @datas << data
    @datas << "\r\n"
  end

  # ファイルを追加する。
  def add_file( file_path, headers )
    add( File.open(file_path), headers)
  end

  def read( length=nil )
    buff = ""
    while ( length==nil || buff.length < length )
      if @io == nil
        @io = next_io()
      end
      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 close
   if (@io != nil)
     @io.close if !@io.closed?
     @io = nil
   end
   @datas.each { |data|
      if data.is_a? IO
        data.close if !data.closed?
      end
   }
  end

private
  def next_io
    @index+=1
    if ( @index == 0)
      @datas <<  "--#{@boundary}--\r\n"
    end
    if ( @index < @datas.length )
      obj = @datas[@index]
      return obj.is_a?(IO) ? obj : StringIO.new(obj)
    else
      return nil
    end
  end
end


参考: RFC1867 HTMLにおける書式からのファイル上納