16 Stimmen

Rails: On-the-fly Streaming von Ausgabe im Zip-Format?

Ich muss einige Daten aus meiner Datenbank in einer Zip-Datei bereitstellen, sie auf dem Flug streamen, so dass:

  • Ich schreibe keine temporäre Datei auf die Festplatte
  • Ich komponiere nicht die ganze Datei im RAM

Ich weiß, dass ich die Streaming-Generierung von Zip-Dateien auf das Dateisystem mit ZipOutputStream wie hier machen kann. Ich weiß auch, dass ich ein Streaming-Output von einem Rails-Controller machen kann, indem ich response_body auf ein Proc setze wie hier. Was ich brauche (denke ich), ist eine Möglichkeit, diese beiden Dinge miteinander zu verbinden. Kann ich Rails dazu bringen, eine Antwort von einem ZipOutputStream zu senden? Kann mir ZipOutputStream inkrementelle Datenblöcke geben, die ich in meinen response_body Proc einspeisen kann? Oder gibt es einen anderen Weg?

0 Stimmen

ZipOutputStream kann dies nicht tun, weil es beim Schreiben der komprimierten Daten vor und zurück durch den Stream sucht (siehe ZipOutputStream#update_local_headers, aufgerufen von ZipOutputStream#close). Daher ist es unmöglich, Datenstücke mit ZipOutputStream zu bedienen, bevor der Vorgang abgeschlossen ist.

11voto

fringd Punkte 2170

Kurzversion

https://github.com/fringd/zipline

Lange Version

so jo5h's Antwort hat bei mir in Rails 3.1.1 nicht funktioniert

Ich habe jedoch ein YouTube-Video gefunden, das geholfen hat.

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

Im Wesentlichen besteht es darin, ein Objekt zu erstellen, das auf 'each' reagiert... Dies ist was ich gemacht habe:

  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                                                                                   

BEARBEITEN:

Die obige Lösung hat TATSÄCHLICH nicht funktioniert... Das Problem ist, dass RubyZip durch die Datei springen muss, um die Header für die Einträge neu zu schreiben, während es weitergeht. Insbesondere muss es die komprimierte Größe VOR dem Schreiben der Daten schreiben. Dies ist in einer wirklich streamenden Situation einfach nicht möglich... Letztendlich könnte diese Aufgabe also unmöglich sein. Es besteht die Möglichkeit, dass es möglich ist, eine ganze Datei gepuffert zu verarbeiten, aber dies schien weniger sinnvoll. Letztendlich habe ich einfach in eine tmp-Datei geschrieben... auf Heroku kann ich nach Rails.root/tmp schreiben, weniger sofortiges Feedback, und nicht ideal, aber notwendig.

NOCH EINE BEARBEITUNG:

Ich hatte vor kurzem eine andere Idee... wir KÖNNTEN die komprimierte Größe der Dateien kennen, wenn wir sie nicht komprimieren. Der Plan sieht etwa wie folgt aus:

Verwenden Sie die ZipStreamOutput-Klasse wie folgt:

  • Verwenden Sie immer die Methode "stored" zur Kompression, komprimieren Sie also nicht
  • Stellen Sie sicher, dass wir nie rückwärts springen, um Dateiheader zu ändern, alles vorne richtig machen
  • Schreibe jeden Code um, der sich auf TOC bezieht, der sucht

Ich habe das noch nicht versucht zu implementieren, werde aber zurückmelden, wenn es Erfolg gibt.

OK EINE LETZTE BEARBEITUNG:

In der Zip-Standard: http://en.wikipedia.org/wiki/Zip_(file_format)#File_headers

erwähnen sie, dass es ein Bit gibt, mit dem man die Größe, die komprimierte Größe und die CRC NACH einer Datei platzieren kann. Also war mein neuer Plan, ZipOutputStream zu unterklassifizieren, sodass es

  • diese Flagge setzt
  • Größen und CRCs nach den Daten schreibt
  • die Ausgabe niemals zurückspult

Außerdem musste ich alle Hacks in Bezug auf das Streamen von Ausgaben in Rails in Ordnung bringen...

Wie auch immer, es hat alles funktioniert!

Hier ist ein Juwel!

https://github.com/fringd/zipline

3voto

j05h Punkte 79

Ich hatte ein ähnliches Problem. Ich brauchte nicht direkt zu streamen, hatte aber nur deinen ersten Fall, in dem ich keine temporäre Datei schreiben wollte. Du kannst ZipOutputStream einfach so modifizieren, dass es ein IO-Objekt anstelle eines Dateinamens akzeptiert.

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

    def stream
      @outputStream
    end
  end
end

Von da an sollte es nur noch darum gehen, den neuen Zip::IOOutputStream in deiner Proc zu verwenden. In deinem Controller würdest du wahrscheinlich etwas Ähnliches tun:

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 Stimmen

Dies funktioniert nicht von selbst ... Zip-Dateien erwarten Größe, komprimierte Größe und einen CRC vor den Daten ... Dieser Code erstellt nur die Datei im Speicher, und der Server wartet immer noch, bis er fertig ist, um mit dem Senden zu beginnen. Verwenden Sie mein Juwel github.com/fringd/zipline

3voto

noel Punkte 2055

Es ist jetzt möglich, dies direkt zu tun:

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
    # oder eine andere Rückgabe von send_data
  end
end

0voto

Taryn East Punkte 26660

Dies ist der Link den Sie wollen:

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

Es baut und generiert die Zipdatei mit ZipOutputStream und sendet sie dann direkt aus dem Controller heraus mit send_file.

0 Stimmen

Keineswegs. Die Frage besagt "so, dass ... Ich keine temporäre Datei auf die Festplatte schreibe". Dieses Beispiel erstellt eine temporäre Datei. Es ist auch mehr oder weniger identisch mit dem ersten Link in der Frage.

0 Stimmen

Die Frage legt fest, dass die temporäre Datei nicht auf die Festplatte geschrieben wird. Die vernünftige Annahme besteht darin, dass Sie nicht möchten, dass sich temporäre Dateien in einem zufälligen Verzeichnis stapeln müssen, um zerstört zu werden. Die Lösung sieht vor, die temporäre Datei unmittelbar nach ihrer Verwendung zu zerstören. Falls eine alternative Annahme vorliegt, lassen Sie es uns bitte wissen - oder Ihre Frage ist nicht vollständig.

0 Stimmen

Wie es ist - deine beiden Anforderungen sind fast gegensätzlich. Entweder ist es auf der Festplatte oder im RAM... also was ist es, was du wirklich willst und warum?

0voto

Konstantin Punkte 2693

Verwenden Sie die gestückelte HTTP-Übertragungskodierung für die Ausgabe: HTTP-Header "Transfer-Encoding: chunked" und strukturieren Sie die Ausgabe gemäß der Spezifikation der gestückelten Codierung um, sodass die resultierende ZIP-Dateigröße am Anfang der Übertragung nicht bekannt sein muss. Kann leicht in Ruby mit Hilfe von Open3.popen3 und Threads codiert werden.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X