Rails の send_data で Windows 用の zip ファイルを送る
Windows 用にファイル名の文字コードを CP932 にしたいわけですが、ハマりどころが多かったのでメモです。
次のようなコードで send_data (not send_file) に指定する zip データを生成可能です。
require 'zip'
require 'zip/version'
require 'open-uri'
def image_data
@image_data ||= open('http://dev.abicky.net/img/favicon.png').read
end
puts "rubyzip version: #{Zip::VERSION}"
if Zip::VERSION < '1.2.0'
module Zip
class Deflater
module EncodingSetter
def initialize(*args)
super
@buffer_stream.set_encoding(@output_stream.external_encoding)
end
end
prepend EncodingSetter
end
end
end
def zip_data_using_write_buffer
io = StringIO.new
io.set_encoding(Encoding::CP932)
Zip::OutputStream.write_buffer(io) { |out|
out.put_next_entry('テスト.png'.encode(Encoding::CP932))
out.write(image_data)
}.string
end
解説
Zip::OutputStream.write_buffer には Encoding::CP932 の StringIO を渡す
そうしないと “local header size changed (43 -> 40) (Zip::Error)” のようなエラーが出ます。
これは、StringIO の文字コードは UTF-8 であり、CP932 のファイル名を指定しても勝手に UTF-8 に変換されてしまうことが原因です。それによって、StringIO の情報から出したヘッダーサイズと、与えられたファイル名(CP932 のファイル名)のバイト数から出したヘッダーサイズが異なるようになり、エラーになります。
cf. https://github.com/rubyzip/rubyzip/blob/v1.2.0/lib/zip/entry.rb#L128
[1] pry(main)> io = StringIO.new; io << 'あ'; io.tell
=> 3
[2] pry(main)> io = StringIO.new; io.set_encoding(Encoding::CP932); io << 'あ'; io.tell
=> 2
rubyzip のバージョンが 1.2.0 より前の場合は Zip::Deflater にパッチを当てる
そうしないと “incompatible character encodings: Windows-31J and UTF-8 (Encoding::CompatibilityError)” のようなエラーが出ます。
これは、Zip::Deflater が Zlib::Deflate#deflate の結果を独自に用意した StringIO に格納しているせいで、UTF-8 に変換されてしまうことが原因です。1.2.0 は実装が変わっているのでこの問題は起きません。
cf. https://github.com/rubyzip/rubyzip/blob/v1.1.7/lib/zip/deflater.rb
もっとシンプルにやればいいんじゃないの?
パフォーマンスが求められないのであれば、次のような書き方が可能です。この場合、zip ファイルが作成されるので、send_data ではなく send_file を使うと思いますが。
def zip_data_using_open
Tempfile.open('') do |tmp|
Zip::OutputStream.open(tmp) do |out|
out.put_next_entry('テスト.png'.encode(Encoding::CP932))
out.write(image_data)
end
File.read(tmp)
end
end