Edited at
foobarDay 8

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

More than 3 years have 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?