Rails:如何实现zip格式的即时流输出?

16
我需要从数据库中提供一些数据的 zip 文件,并在传输时进行流式处理,以便:
  • 不将临时文件写入磁盘
  • 不在 RAM 中组合整个文件
我知道可以使用 ZipOutputStream在此处)将 zip 文件流生成到文件系统。 我还知道可以通过将 response_body 设置为 Proc在此处)从 rails 控制器进行流输出。 我需要的是(我想)一种将这两个东西结合起来的方法。 我能否让 rails 提供 ZipOutputStream 的响应? 我能否获得 ZipOutputStream 给我的增量数据块并将其馈送到我的 response_body Proc 中? 还是有其他方式?

ZipOutputStream无法这样做,因为在写入压缩数据时,它会在流中来回寻找(请参见ZipOutputStream#update_local_headers,从ZipOutputStream#close调用)。因此,在操作完成之前,使用ZipOutputStream服务数据块是不可能的。 - Rômulo Ceccon
5个回答

11

简短版

https://github.com/fringd/zipline

详细版

在Rails 3.1.1中,jo5h的答案对我没有用。

但我找到了一个YouTube视频来帮助我。

http://www.youtube.com/watch?v=K0XvnspdPsc

关键是创建一个响应each方法的对象... 这是我所做的:

  class ZipGenerator                                                                    
    def initialize(model)                                                               
      @model = model                                                                    
    end                                                                                 
                                                                                        
    def each( &block )                                                                  
      output = Object.new                                                               
      output.define_singleton_method :tell, Proc.new { 0 }                              
      output.define_singleton_method :pos=, Proc.new { |x| 0 }                          
      output.define_singleton_method :<<, Proc.new { |x| block.call(x) }                
      output.define_singleton_method :close, Proc.new { nil }                           
      Zip::IoZip.open(output) do |zip|                                                  
        @model.attachments.all.each do |attachment|                                     
          zip.put_next_entry "#{attachment.name}.pdf"                                   
          file = attachment.file.file.send :file                                        
          file = File.open(file) if file.is_a? String                                   
          while buffer = file.read(2048)                                                
            zip << buffer                                                               
          end                                                                           
        end                                                                             
      end                                                                               
      sleep 10                                                                          
    end                                                                                 
                                                                                        
  end
                                                                                  
  def getzip                                                                            
    self.response_body = ZipGenerator.new(@model)                                       
                                                                                        
    #this is a hack to preven middleware from buffering                                 
    headers['Last-Modified'] = Time.now.to_s                                            
  end                                                                                   

编辑:

上面的解决方案实际上并没有起作用...问题在于RubyZip需要在进行条目头部的重写时跳转到文件中。特别是,它需要在写入数据之前写入压缩大小。在真正的流式情况下这是不可能的...所以最终这个任务可能是不可能完成的。有可能一次性缓冲整个文件,但这似乎不值得去做。最终我只是写到了一个临时文件里...在Heroku上,我可以将文件写入Rails.root/tmp,虽然反馈不够即时,也不是一个理想的解决办法,但是必要的。

另一个编辑:

我最近又想到了一个想法...如果我们不压缩文件,我们就能知道文件的压缩大小。计划大致如下:

将ZipStreamOutput类作为子类实现以下功能:

  • 始终使用“存储”压缩方法,换句话说不压缩
  • 确保我们从不向后寻找以更改文件头,一开始就把它搞定
  • 重写与TOC相关的任何代码

我还没有尝试实现这个,但如果成功了,我会报告回来。

好吧,最后一次编辑:

在Zip标准中:http://en.wikipedia.org/wiki/Zip_(file_format)#File_headers

他们提到有一个位可以翻转,将大小,压缩大小和CRC放在文件之后。所以我的新计划是子类化zipoutput流,使其

  • 设置此标志
  • 在数据之后写入大小和CRC
  • 永不倒带输出

此外,我需要解决所有关于Rails的流式输出的问题...

总之这一切都奏效了!

这里有一个gem!

https://github.com/fringd/zipline


3

我有一个类似的问题。我不需要直接进行流传输,但只是遇到了第一种情况,即不想编写临时文件。您可以轻松修改ZipOutputStream以接受IO对象而不仅仅是文件名。

module Zip
  class IOOutputStream < ZipOutputStream
    def initialize io
      super '-'
      @outputStream = io
    end

    def stream
      @outputStream
    end
  end
end

接下来,只需要在您的Proc中使用新的Zip::IOOutputStream即可。在您的控制器中,您可能会这样做:

self.response_body =  proc do |response, output|
  Zip::IOOutputStream.open(output) do |zip|
    my_files.each do |file|
      zip.put_next_entry file
      zip << IO.read file
    end
  end
end

3
这本身是行不通的……zip文件需要在数据之前提供大小、压缩大小和CRC校验码……这段代码只是在内存中构建文件,服务器仍然要等到它完成才开始发送。使用我的gem https://github.com/fringd/zipline。 - fringd

3
现在可以直接这样做:
class SomeController < ApplicationController
  def some_action
    compressed_filestream = Zip::ZipOutputStream.write_buffer do |zos|
      zos.put_next_entry "some/filename.ext"
      zos.print data
    end
    compressed_filestream .rewind
    respond_to do |format|
      format.zip do
        send_data compressed_filestream .read, filename: "some.zip"
      end
    end
    # or some other return of send_data
  end
end

0

不行。问题指定“这样……我不会将临时文件写入磁盘”。那个例子创建了一个临时文件。它也或多或少与问题中的第一个链接相同。 - kdt
问题指定临时文件不写入磁盘。合理的假设是您不希望在某个随机目录中堆积临时文件 - 必须被销毁。所提供的解决方案在使用临时文件后立即销毁它。如果有其他假设,请告诉我们 - 或者您的问题不完整。 - Taryn East
你提出的两个要求几乎是互相矛盾的。它只能在磁盘上或者在内存中,所以你真正想要什么?为什么? - Taryn East
4
@TarynEast,您可以使用仅有100MB RAM和100MB硬盘的服务器来压缩/发送整个DVD。这意味着立即发送压缩后的内容,而不是流式传输。因此,kdt的需求并不相互排斥。也许kdt想要使用价格不太昂贵的服务器高效地发送大量数据。另一个优点是压缩和下载时间是并行的,而不是相加的。干杯! - Nicolas Raoul

0

使用分块的HTTP传输编码进行输出:HTTP头“Transfer-Encoding: chunked”,并根据分块编码规范重新构造输出,因此在传输开始时无需知道结果ZIP文件的大小。可以借助Open3.popen3和线程轻松地在Ruby中编写。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接