Help us understand the problem. What is going on with this article?

Webサーバのファイルを、HTTP Range Headerを使ってRuby IOクラスに擬態化させて読み込む

はじめに

Slackのruby-jpで、S3上のデカいzipファイルを解凍しながらダウンロードしたいと書いてる人がいて、それに対してHTTP Range Headerをいい感じにするIOを作るというアイデアを提案してる人がいたので作ってみた。

ちなみに s3io というgemがまさにそれなのだが、aws-sdkに依存している(しかも古いバージョンのまま更新されてない)ので、HTTP Range Headerを直接使ってS3以外でも使えるものを作った。

完成品

rangeio.rb
require 'net/http'
require 'uri'

class HTTPRangeIO

    def initialize(url, clazz = Net::HTTP)
        uri = URI.parse(url)
        @http = clazz.start(uri.hostname, uri.port, use_ssl:(uri.scheme == 'https'))
        @path = uri.request_uri
        @pos = 0
    end

    def tell
        @pos
    end

    def seek(offset, whence = IO::SEEK_SET)
        case whence
        when IO::SEEK_CUR
            @pos += offset
        when IO::SEEK_END
            @pos = size + offset
        else
            @pos = offset
        end
        raise Errno::EINVAL if @pos < 0
    end

    def size
        @content_length ||= @http.request_head(@path).content_length
    end

    def read(length = nil, outbuf = "")
        first = @pos
        last = length ? (@pos + length) : size
        return "" if first == last
        request = Net::HTTP::Get.new(@path)
        request.range = Range.new(first, last, true)
        response = @http.request(request)
        @pos += response.content_length
        outbuf.replace(response.body)
    end

    def close
        @http.finish unless @cloned
    end

    def initialize_copy(obj)
        @cloned = true
        super
    end

    def method_missing(name, *args)
        # do nothing
    end

end

IOと言いながら読み込みにしか対応してないのがダサいところではあるが、今回の要件では tell, seek, size, read, close を実装すれば動いたのでこれだけで。
initialize_copy はdupされたインスタンスがcloseされたときにセッションをクローズしないようにするため、method_missing はサポートされてない(主に書き込み用の)メソッドを呼ばれても落ちないようにするためにある。

使い方

考え方は StringIO と同じで、URLをコンストラクタに渡すと、それをIOとして扱うことができるようになる。

プログラム

sample.rb
require 'zip'
require 'rangeio'

url = "https://s3.amazonaws.com/aws-cli/awscli-bundle.zip"
io = HTTPRangeIO.new(url)
zf = Zip::File.new(io, false, true)
zf.each do |entry|
    puts entry.name
end
zf.close
io.close

ここではzipライブラリとして rubyzip を使っているが、読み込み専用の Zip::InputStream を使ってもおそらく問題はない。
Zip::File.open_buffer はIOを引数にとるが、zipファイルを更新するためのメソッドなので、読み込みのみでも書き込み用のメソッドが走ってしまうらしいので、使わないことにした。

実行結果

$ ruby sample.rb
awscli-bundle/install
awscli-bundle/packages/urllib3-1.22.tar.gz
awscli-bundle/packages/PyYAML-3.13.tar.gz
awscli-bundle/packages/futures-3.3.0.tar.gz
awscli-bundle/packages/python-dateutil-2.8.0.tar.gz
awscli-bundle/packages/s3transfer-0.2.1.tar.gz
awscli-bundle/packages/colorama-0.4.1.tar.gz
awscli-bundle/packages/six-1.12.0.tar.gz
awscli-bundle/packages/colorama-0.3.9.tar.gz
awscli-bundle/packages/PyYAML-5.1.2.tar.gz
awscli-bundle/packages/python-dateutil-2.6.1.tar.gz
awscli-bundle/packages/rsa-3.4.2.tar.gz
awscli-bundle/packages/argparse-1.2.1.tar.gz
awscli-bundle/packages/simplejson-3.3.0.tar.gz
awscli-bundle/packages/awscli-1.16.256.tar.gz
awscli-bundle/packages/docutils-0.15.2.tar.gz
awscli-bundle/packages/pyasn1-0.4.7.tar.gz
awscli-bundle/packages/virtualenv-15.1.0.tar.gz
awscli-bundle/packages/urllib3-1.25.6.tar.gz
awscli-bundle/packages/botocore-1.12.246.tar.gz
awscli-bundle/packages/jmespath-0.9.4.tar.gz
awscli-bundle/packages/ordereddict-1.1.tar.gz
awscli-bundle/packages/setup/setuptools_scm-1.15.7.tar.gz
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away