2013-02-16

zip を作りながら順次送信する

Sinatra では Sinatra::Streaming (Sinatra API Documentation) を用いてレスポンスボディをちょっとずつ返す (Sinatra 1.3.0 & Padrino 0.10.3 がリリースされました。ざっくり紹介(1)。 « blog.udzura.jp) ことができる.

テキストコンテンツではうまく動いた.次の例に web ブラウザから接続すると順次表示されていく.

require 'sinatra/streaming'
helpers Sinatra::Streaming
get '/stream-text' do
  stream do |out|
    0.upto( 1000 ) do |i|
      out << i
      out.flush
      sleep 1
    end
  end
end

archive-zipCreate a new archive which will be written to a pipe することができる.ところがこの stream にはうまく出力できない.次の例では,壊れた zip がダウンロードされる.

require 'rubygems'
require 'archive/zip'
require 'archive/zip/entry'
require 'sinatra/streaming'
helpers Sinatra::Streaming
get '/stream.zip' do
  content_type 'application/zip'
  stream do |out|
    Archive::Zip.open( out, :w ) do |zip|
      0.upto( 1000 ) do |i|
        zip.add_entry( Archive::Zip::Entry.from_file( $0, :zip_path => "#{i}" ) )
      end
    end
    out.close
  end
end
$ zip -T stream.zip 
 zip warning: local and central headers differ for (null)
 zip warning:  offset 10--local = 00, central = 48
 zip warning:  offset 11--local = 00, central = 8b
 zip warning:  offset 12--local = 00, central = fd
 zip warning:  offset 13--local = 00, central = 01
 zip warning:  offset 14--local = 00, central = 31
 zip warning:  offset 15--local = 00, central = 01
 zip warning:  offset 18--local = 00, central = d7
 zip warning:  offset 19--local = 00, central = 01

zip error: Zip file structure invalid (stream-direct.zip)

次のように間に IO::pipe を入れるとうまくいく.追及していない.

require 'rubygems'
require 'archive/zip'
require 'archive/zip/entry'
require 'sinatra/streaming'
helpers Sinatra::Streaming
get '/stream-pipe.zip' do
  content_type 'application/zip'
  stream do |out|
    reader, writer = IO.pipe
    write_t = Thread.new do
      Thread.pass
      Archive::Zip.open( writer, :w ) do |zip|
        0.upto( 1000 ) do |i|
          zip.add_entry( Archive::Zip::Entry.from_file( $0, :zip_path => "#{i}" ) )
        end
      end
      writer.close
    end
    until reader.eof?
      out << reader.readpartial( 4 ) # optimize it                                                  
    end
  end
end
$ zip -T stream-pipe.zip 
test of stream-pipe.zip OK

(2013-09-21 追記) IO#readpartial を用いるととても遅い.IO#sysread も同様.IO#read(十分に大きい値) ならばずっと速いが,ブロックしてしまうので,あまり大きな値を引数に与えると未完了でも HTTP サーバが接続を切ってしまう.結局,うまい方法は見出せていない.