Rails: потоковая передача на лету в формате zip? - PullRequest
15 голосов
/ 25 января 2011

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

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

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

Ответы [ 5 ]

11 голосов
/ 26 октября 2011

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

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                                                                                   

РЕДАКТИРОВАТЬ:

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

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

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

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

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

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

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

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

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

вот драгоценный камень!

https://github.com/fringd/zipline

3 голосов
/ 03 марта 2011

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

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 голосов
/ 24 февраля 2016

Теперь это можно сделать напрямую:

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 голосов
/ 14 мая 2016

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

0 голосов
/ 15 февраля 2011

Это ссылка, которую вы хотите:

http://info.michael -simons.eu / 2008/01/21 / с использованием-rubyzip к созданию-ZIP-файлов, на лету /

Он создает и генерирует zip-файл, используя ZipOutputStream, а затем использует send_file, чтобы отправить его напрямую из контроллера.

...