2011-01-25 12 views
14

ho bisogno di servire alcuni dati dal mio database in un file zip, in streaming al volo in modo tale che:Rails: streaming al volo dell'output in formato zip?

  • non scrivo un file temporaneo sul disco
  • Non compongo l'intero file in RAM

so che posso fare generazione lo streaming di file zip al filesystemk utilizzando ZipOutputStream come here. So anche che posso eseguire l'output in streaming da un controller delle guide impostando response_body su Proc come here. Quello di cui ho bisogno (penso) è un modo per collegare queste due cose insieme. Posso fare in modo che le rotaie rispondano da un ZipOutputStream? Posso ottenere ZipOutputStream dammi quantità di dati incrementali che posso alimentare nel mio response_bodyProc? oppure c'è un'altro modo?

+0

ZipOutputStream non può farlo perché cerca avanti e indietro attraverso lo stream mentre scrive i dati compressi (vedere 'ZipOutputStream # update_local_headers', chiamato da' ZipOutputStream # close'). Pertanto, è impossibile fornire blocchi di dati con ZipOutputStream prima che l'operazione venga completata. –

risposta

3

Ho avuto un problema simile. Non avevo bisogno di eseguire lo streaming direttamente, ma avevo solo il tuo primo caso di non voler scrivere un file temporaneo. È possibile modificare facilmente ZipOutputStream per accettare un oggetto IO anziché solo un nome file.

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

    def stream 
     @outputStream 
    end 
    end 
end 

Da lì, dovrebbe essere solo una questione di utilizzare il nuovo Zip :: IOOutputStream in Proc. Nel vostro controller, si sarebbe probabilmente fare qualcosa di simile:

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

questo non funziona da solo ... i file zip prevedono dimensioni, dimensioni_ compresse e un CRC prima dei dati ... questo codice crea semplicemente il file in memoria e il server attende fino a quando non ha finito di iniziare l'invio. usa la mia gemma https://github.com/fringd/zipline – fringd

0

questo è il link che si desidera:

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

Costruisce e genera il file zip con ZipOutputStream e poi send_file utilizza per inviare direttamente dal controller.

+0

No. La domanda specifica "tale che ... non scrivo un file temporaneo su disco". Questo esempio crea un file temporaneo. È anche più o meno identico al primo link nella domanda. – kdt

+0

La domanda specifica che il file temporaneo non è stato scritto sul disco. Il presupposto ragionevole è che non si desidera che i file temporanei si accumulino in qualche directory casuale, poiché devono essere distrutti. La soluzione fornita distrugge il file temporaneo immediatamente dopo il suo utilizzo. Se c'è un'ipotesi alternativa, faccelo sapere - o le tue domande non sono complete. –

+0

Così com'è, le tue due esigenze sono quasi reciprocamente esclusive. O è su disco, o è in RAM ... quindi cosa vuoi veramente e perché? –

10

Versione breve

https://github.com/fringd/zipline

Long Version

così la risposta di jo5h non ha funzionato per me in rotaie 3.1.1

ho trovato un video di youtube che ha aiutato, però.

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

il nocciolo sta creando un oggetto che risponde ad ogni ... questo è quello che ho fatto:

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:

soluzione sopra non ha effettivamente lavoro ... il problema è che rubyzip ha bisogno di saltare il file per riscrivere le intestazioni per le voci come va. in particolare ha bisogno di scrivere la dimensione compressa PRIMA che scriva i dati. questo non è possibile in una vera situazione di streaming ... quindi alla fine questo compito potrebbe essere impossibile. c'è la possibilità che possa essere possibile bufferizzare un intero file alla volta, ma questo sembrava meno ne valeva la pena. alla fine ho appena scritto un file tmp ... su heroku posso scrivere su Rails.root/tmp meno feedback istantanei, e non ideale, ma necessario.

ALTRO EDIT:

ho avuto un'altra idea di recente ... potremmo conoscere la dimensione compressa dei file, se non li impacco. il piano più o meno così:

sottoclasse della classe ZipStreamOutput come segue:

  • utilizzare sempre il metodo di compressione "memorizzato", in altre parole non comprimere
  • assicurare che non abbiamo mai chiedere indietro per modificare il file intestazioni, ottenere tutto fino davanti
  • riscrivere qualsiasi codice relativo al sommario che cerca

non ho cercato di attuare questo ancora, ma sarà riferire se ci è un successo.

OK ONE Ultima modifica:

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

accennano che c'è un po 'si può capovolgere di mettere la dimensione, la dimensione compressa e crc dopo che un file. così il mio nuovo piano era quello di creare una sottoclasse zipoutput flusso in modo che

  • set di questo flag
  • scrive dimensioni e CRC dopo i dati
  • mai riavvolge uscita

, inoltre, avevo bisogno di ottenere tutte le hack per lo streaming di output su binari riparati ...

comunque ha funzionato!

ecco un gioiello!

https://github.com/fringd/zipline

1

È ora possibile farlo direttamente:

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

Usa codifica di trasferimento Chunked HTTP per l'output: HTTP header "Transfer-Encoding: Chunked" e ristrutturare l'uscita in base al chunked specifica di codifica, quindi non è necessario conoscere la dimensione del file ZIP risultante all'inizio del trasferimento. Può essere facilmente codificato in Ruby con l'aiuto di Open3.popen3 e thread.

Problemi correlati