Ruby
pry
Pry-doc
foobarDay 8

Rubyのopen_uriでファイルオブジェクトがTempfileになる瞬間を追った

More than 1 year has passed since last update.

動機

外部ファイルをOpenURI#open_uriで取得したオブジェクトがStringIOで返ってきたりTempfileで返ってきたりした。

リファレンス

http://docs.ruby-lang.org/ja/2.2.0/library/open=2duri.html

開いたファイルオブジェクトは StringIO もしくは Tempfile ですが OpenURI::Meta モジュールで拡張されていて、メタ情報を獲得する メソッドが使えます。

環境

  • ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-darwin14]
  • Pry version 0.10.1 on Ruby 2.2.2

調査方法

  • pry-docでソースコードを読む

OpenURI.open_uri

> require 'open-uri'
> show-source OpenURI.open_uri
def OpenURI.open_uri(name, *rest) # :nodoc:
  # URIのパース処理とか
  io = open_loop(uri, options)
  # 最終的にioが戻り値
end

URIの処理とかエンコーディングとかやってるけど最終的に返ってくるオブジェクトはopen_loop内で作られてる

OpenURI.open_loop

> show-source OpenURI.open_loop
def OpenURI.open_loop(uri, options) # :nodoc:
  # プロキシとかの処理
  uri_set = {}
  buf = nil
  while true
    redirect = catch(:open_uri_redirect) {
      buf = Buffer.new
      uri.buffer_open(buf, find_proxy.call(uri), options)
      nil
    }
  # リダイレクトの例外をキャッチした場合の処理
  end
  io = buf.io
  io.base_uri = uri
  io
end

戻り値ioの中身がbuf.ioらしいのでBufferの中を見てみる。

OpenURI::Buffer

> show-source OpenURI::Buffer
class Buffer # :nodoc: all
  def initialize
    @io = StringIO.new
    @size = 0
  end
  attr_reader :size

  StringMax = 10240
  def <<(str)
    @io << str
    @size += str.length
    if StringIO === @io && StringMax < @size
      require 'tempfile'
      io = Tempfile.new('open-uri')
      io.binmode
      Meta.init io, @io if Meta === @io
      io << @io.string
      @io = io
    end
  end

  def io
    Meta.init @io unless Meta === @io
    @io
  end
end

お目当てのStringIOTempfileを生成しているところを見つけました。
initializeの中でまずStringIOオブジェクトとして定義してます。
そして<<メソッドでTempfileオブジェクトとして再定義してます。
このメソッドが呼ばれる箇所を見てみます。

URI::HTTP#buffer_open

> show-source URI::HTTP#buffer_open
def buffer_open(buf, proxy, options) # :nodoc:
  OpenURI.open_http(buf, self, proxy, options)
end

OpenURI.open_http

> show-source OpenURI.open_http
def OpenURI.open_http(buf, target, proxy, options) # :nodoc:
  # プロキシとかの処理 
  http.start {
    req = Net::HTTP::Get.new(request_uri, header)
    if options.include? :http_basic_authentication
      user, pass = options[:http_basic_authentication]
      req.basic_auth user, pass
    end
    http.request(req) {|response|
      resp = response
      if options[:content_length_proc] && Net::HTTPSuccess === resp
        if resp.key?('Content-Length')
          options[:content_length_proc].call(resp['Content-Length'].to_i)
        else
          options[:content_length_proc].call(nil)
        end
      end
      resp.read_body {|str|
        buf << str
        if options[:progress_proc] && Net::HTTPSuccess === resp
          options[:progress_proc].call(buf.size)
        end
      }
    }
  }
  io = buf.io
  io.rewind
  io.status = [resp.code, resp.message]
  resp.each_name {|name| buf.io.meta_add_field2 name, resp.get_fields(name) }
  # リダイレクトだった時の処理とか
end

buf << strBuffer<<メソッドが呼ばれてました。
HTTP通信に成功したらそのファイルの文字列長によって返却されるオブジェクトが変わるんですね。

参考
Why does OpenURI treat files under 10kb in size as StringIO?