Rails:以 zip 格式即时传输输出?

发布于 2024-10-14 13:10:12 字数 619 浏览 4 评论 0原文

我需要在 zip 文件中提供数据库中的一些数据,动态流式传输,以便:

  • 我不会将临时文件写入磁盘
  • 我不会在 RAM 中编写整个文件

我知道我可以流式生成 zip使用 ZipOutputStream 作为 这里。我还知道我可以通过将 response_body 设置为 Proc 作为 此处。我认为我需要的是一种将这两件事结合在一起的方法。我可以让 Rails 提供来自 ZipOutputStream 的响应吗?我可以让 ZipOutputStream 为我提供增量数据块,并将其输入到我的 response_body Proc 中吗?或者还有别的办法吗?

I need to serve some data from my database in a zip file, streaming it on the fly such that:

  • I do not write a temporary file to disk
  • I do not compose the whole file in RAM

I know that I can do streaming generation of zip files to the filesystemk using ZipOutputStream as here. I also know that I can do streaming output from a rails controller by setting response_body to a Proc as here. What I need (I think) is a way of plugging those two things together. Can I make rails serve a response from a ZipOutputStream? Can I get ZipOutputStream give me incremental chunks of data that I can feed into my response_body Proc? Or is there another way?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

骑趴 2024-10-21 13:10:12

短版本

https://github.com/fringd/zipline

长版本

所以 jo5h 的答案不起作用 不过,在 Rails 3.1.1 中,

我发现了一个 YouTube 视频对我有帮助。

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

问题的关键正在创建一个响应每个对象的对象...这就是我所做的:

  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                                                                                   

编辑:

上面的解决方案实际上不起作用...问题是 ruby​​zip 需要在文件中跳转以重写条目的标头。特别是它需要在写入数据之前写入压缩大小。这在真正的流媒体情况下是不可能的......所以最终这个任务可能是不可能的。有可能一次缓冲整个文件,但这似乎不太值得。最终我只是写入了一个 tmp 文件...在heroku 上我可以写入 Rails.root/tmp 较少的即时反馈,虽然不理想,但有必要。

另一个编辑:

我最近有了另一个想法......如果我们不压缩文件,我们可以知道文件的压缩大小。计划是这样的:

按如下方式对 ZipStreamOutput 类进行子类化:

  • 始终使用“存储”压缩方法,换句话说,不压缩
  • 确保我们永远不会向后查找以更改文件头,预先将其全部正确
  • 重写与TOC 寻求

我还没有尝试实现这一点,但如果有任何成功,我会报告。

确定最后一次编辑:

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

他们提到,您可以翻转一些内容,将大小、压缩大小和 crc 放在文件后面。所以我的新计划是对 zipoutput 流进行子类化,以便

  • 后设置此标志
  • 在数据
  • 永远不会倒回输出

写入大小和 CRC ,而且我需要获得所有 hack 以便在修复的 Rails 中流式输出...

无论如何,这一切都有效!

这是一颗宝石!

https://github.com/fringd/zipline

Short Version

https://github.com/fringd/zipline

Long Version

so jo5h's answer didn't work for me in rails 3.1.1

i found a youtube video that helped, though.

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

the crux of it is creating an object that responds to each... this is what i did:

  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                                                                                   

EDIT:

the above solution didn't ACTUALLY work... the problem is that rubyzip needs to jump around the file to rewrite the headers for entries as it goes. particularly it needs to write the compressed size BEFORE it writes the data. this is just not possible in a truly streaming situation... so ultimately this task may be impossible. there is a chance that it might be possible to buffer a whole file at a time, but this seemed less worth it. ultimately i just wrote to a tmp file... on heroku i can write to Rails.root/tmp less instant feedback, and not ideal, but neccessary.

ANOTHER EDIT:

i got another idea recently... we COULD know the compressed size of the files if we do not compress them. the plan goes something like this:

subclass the ZipStreamOutput class as follows:

  • always use the "stored" compression method, in other words do not compress
  • ensure we never seek backwards to change file headers, get it all right up front
  • rewrite any code related to TOC that seeks

I haven't tried to implement this yet, but will report back if there's any success.

OK ONE LAST EDIT:

In the zip standard: http://en.wikipedia.org/wiki/Zip_(file_format)#File_headers

they mention that there's a bit you can flip to put the size, compressed size and crc AFTER a file. so my new plan was to subclass zipoutput stream so that it

  • sets this flag
  • writes sizes and CRCs after the data
  • never rewinds output

furthermore i needed to get all the hacks in order to stream output in rails fixed up...

anyways it all worked!

here's a gem!

https://github.com/fringd/zipline

献世佛 2024-10-21 13:10:12

我有类似的问题。我不需要直接流式传输,但只有第一种情况不想写入临时文件。您可以轻松修改 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

I had a similar issue. I didn't need to stream directly, but only had your first case of not wanting to write a temp file. You can easily modify ZipOutputStream to accept an IO object instead of just a filename.

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

    def stream
      @outputStream
    end
  end
end

From there, it should just be a matter of using the new Zip::IOOutputStream in your Proc. In your controller, you'd probably do something like:

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
把梦留给海 2024-10-21 13:10:12

现在可以直接执行此操作:

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

It is now possible to do this directly:

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
迷你仙 2024-10-21 13:10:12

这是您想要的链接:

http://info.michael-simons.eu/2008/01/21/using-rubyzip-to-create-zip-files-on-the-fly/

它构建并生成使用 ZipOutputStream 发送 zipfile,然后使用 send_file 将其直接从控制器发送出去。

This is the link you want:

http://info.michael-simons.eu/2008/01/21/using-rubyzip-to-create-zip-files-on-the-fly/

It builds and generates the zipfile using ZipOutputStream and then uses send_file to send it directly out from the controller.

末蓝 2024-10-21 13:10:12

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

Use chunked HTTP transfer encoding for output: HTTP header "Transfer-Encoding: chunked" and restructure the output according to the chunked encoding specification, so no need to know the resulting ZIP file size at the begginning of the transfer. Can be easily coded in Ruby with the help of Open3.popen3 and threads.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文