Rails: потоковая передача на выходе в формате zip?

Мне нужно обслуживать некоторые данные из моей базы данных в zip файле, передавая его на лету так, чтобы:

  • Я не пишу временный файл на диск
  • Я не собираю весь файл в оперативной памяти

Я знаю, что я могу создавать потоковое создание zip файлов в файловой системе, используя ZipOutputStream как здесь. Я также знаю, что я могу делать потоковый вывод с контроллера rails, установив response_body в Proc как здесь. Мне нужно (я думаю) - это способ объединить эти две вещи вместе. Могу ли я сделать рельсы для ответа от ZipOutputStream? Могу ли я получить ZipOutputStream, чтобы получить инкрементные куски данных, которые я могу передать в мой response_body Proc? Или есть другой способ?

Ответ 1

У меня была аналогичная проблема. Мне не нужно было передавать потоки напрямую, но у вас был только первый случай, когда вы не хотели писать временный файл. Вы можете легко изменить ZipOutputStream, чтобы принять объект IO вместо имени файла.

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

    def stream
      @outputStream
    end
  end
end

Оттуда, это просто вопрос использования нового Zip:: IOOutputStream в вашем Proc. В вашем контроллере вы, вероятно, будете делать что-то вроде:

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

Ответ 2

Краткая версия

https://github.com/fringd/zipline

Длинная версия

поэтому ответ jo5h не работал у меня в рельсах 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                                                                                   

EDIT:

вышеупомянутое решение не работало на самом деле... проблема в том, что rubyzip необходимо перескакивать по файлу, чтобы переписать заголовки для записей по мере их появления. в частности, необходимо записать сжатый размер ДО того, как он записывает данные. это просто невозможно в поистине потоковой ситуации... так что в конечном итоге эта задача может быть невозможной. есть вероятность, что можно было бы одновременно буферировать целый файл, но это казалось менее ценным. в конечном счете, я просто написал файл tmp... на heroku я могу написать Rails.root/tmp менее мгновенную обратную связь, а не идеальную, но необходимую.

ДРУГОЙ РЕДАКТИРОВАНИЕ:

У меня недавно появилась другая идея... мы МОЖЕМ знать сжатый размер файлов, если мы не сжимаем их. план выглядит примерно так:

подкласс класса ZipStreamOutput следующим образом:

  • всегда используйте метод "сохраненного" сжатия, другими словами не сжимайте
  • гарантируем, что мы никогда не будем искать назад, чтобы изменить заголовки файлов, получить все в порядке.
  • переписать любой код, относящийся к TOC, который ищет

Я еще не пытался реализовать это, но буду отчитываться, если есть какие-либо успехи.

OK ONE LAST EDIT:

В стандарте zip: http://en.wikipedia.org/wiki/Zip_(file_format)#File_headers

они упоминают, что немного вы можете перевернуть, чтобы поместить размер, сжатый размер и crc ПОСЛЕ файла. поэтому мой новый план заключался в подклассе потока zipoutput, чтобы он

  • устанавливает этот флаг
  • записывает размеры и CRC после данных
  • никогда не перематывает выходные данные

кроме того, мне нужно было получить все хаки, чтобы поток выводился в рельсах, зафиксированных...

в любом случае все это сработало!

здесь драгоценный камень!

https://github.com/fringd/zipline

Ответ 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

Ответ 5

Использовать закодированную кодировку HTTP-передачи для вывода: HTTP-заголовок "Transfer-Encoding: chunked" и реструктурировать вывод в соответствии с спецификацией кодированного кодирования, поэтому не нужно знать полученный размер ZIP файла при начале передачи. Может быть легко закодирован в Ruby с помощью Open3.popen3 и потоков.