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


### 動機
外部ファイルを`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`
```rb
> 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`
```rb
> 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`
```rb
> 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
```

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

#### `URI::HTTP#buffer_open`
```rb
> show-source URI::HTTP#buffer_open
def buffer_open(buf, proxy, options) # :nodoc:
  OpenURI.open_http(buf, self, proxy, options)
end
```
#### `OpenURI.open_http`
```rb
> 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 << str` で`Buffer`の`<<`メソッドが呼ばれてました。
HTTP通信に成功したらそのファイルの文字列長によって返却されるオブジェクトが変わるんですね。

参考
[Why does OpenURI treat files under 10kb in size as StringIO?](http://stackoverflow.com/questions/10496874/why-does-openuri-treat-files-under-10kb-in-size-as-stringio)