Creare un file zip al volo con ruby

Recentemente, mi è stato chiesto di aggiungere ad una applicazione Ruby on Rails una funzionalità per il download di un’intera galleria di immagini in un unico file zip.

In modo abbastanza ovvio ho deciso di sfruttare la gemma rubyzip per creare l’archivio compresso da scaricare.

Ma ciò che volevo era creare lo zip al volo, direttamente in memoria, senza popolare le cartelle della mia applicazione di file zip che avrei poi dovuto rimuovere.

Da qui l’idea di utilizzare Tempfile per creare il file in memoria.

Di seguito la funzione “download_zip” che ho creato:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def download_zip(image_list)
    if !image_list.blank?
      file_name = "pictures.zip"
      t = Tempfile.new("mio-file-temp-#{Time.now}")
      Zip::ZipOutputStream.open(t.path) do |z|
        image_list.each do |img|
          title = img.title
          title += ".jpg" unless title.end_with?(".jpg")
          z.put_next_entry(title)
          z.print IO.read(img.path)
        end
      end
      send_file t.path, :type => 'application/zip',
                             :disposition => 'attachment',
                             :filename => file_name
      t.close
    end
  end

Ecco come lavora la funzione download_zip:

  • prende in input una lista di oggetti Image, dove Image ha le proprietà title e path relative all’immagine.
  • crea il file temporaneo e lo apre come uno ZipOutputStream.
  • per ogni immagine presente nella lista crea un nuovo elemento nello ZipOutputStream con nome uguale al titolo dell’immagine e contenuto letto dal path.
  • successivamente, la chiusura del blocco Zip::ZipOutputStream … end causa automaticamente la chiusura del nuovo file zip.
  • a questo punto non rimane altro che inviare il file all’utente con il corretto mime type.
  • infine, viene chiuso il file temporaneo, che sarà poi rimosso dalla memoria dal garbage collector.

Come vedete, con poche righe di codice si è ottenuta una soluzione molto pulita ed efficace.

Tags: , ,


About Claudio

Claudio Marai is a co-founder of DevInterface.

After graduating in Computer Science has contributed to develop complex web applications based on Java/J2EE and desktop applications with the. NET framework for the Ministry of Justice and ultimately for the banking ambit.

The passion for web in recent years has led him to be interested in more modern frameworks such as Ruby on Rails and Django, and to a development approach based on agile methodologies such as eXtreme Programming and SCRUM.

About DevInterface

We are an information and communication technology agency. Our mission is to provide web application development, design services and communication strategies. We specialize in building web applications with modern and efficient frameworks.

Related Post

22 Responses to “Creare un file zip al volo con ruby”

  1. ramzi scrive:

    Tempfile.new(“my-temp-filename-#{request.remote_ip}”)

    using the client ip address to generate a unique file is flawed…

    what happens if a user has more than one browser open, trying to download pictures?
    also, keep in mind that in many scenarios (universities, large corporations, fastweb), large subnets access the internet using a single IP address.

    a better solution would be http://raa.ruby-lang.org/project/ruby-uuid/

    keep up the great stuff!

  2. Claudio scrive:

    Your observation is correct. Thank you for letting me see this “unforgivable” mistake. I’ve adjusted the example using a simple but effective workaround:

    Tempfile.new ( “my-temp-filename-request.remote_ip # ()”)

    now becomes

    Tempfile.new ( “my-temp-filename-# (Time.now)”)

    so that the name is no more linked to the ip-address but rather to the download-time.

  3. Justin Powers scrive:

    Excellent tip, I will be using this for my website! My only comment is that it is still possible for two people to request zip files at the same time (Time.now is accurate to the second), so I might either use a UUID (as Ramzi suggested), or just add a random number, like so:

    Tempfile.new(“my-temp-filename-#{Time.now.to_s + rand(9999).to_s}”

    Also, since the files exist only in memory, and therefore likely only last a short amount of time, Time.now provides very little entropy. Just a thought!

  4. Tonya Henson scrive:

    You can also go to open zip file to open your zip files online. I hope it would be a great help.

  5. Ricardo Duarte scrive:

    Claudio,

    I’ve got this error when using your code at line 12:

    wrong argument type String (expected Zip::Archive)

    11. t = Tempfile.new(“my-temp-filename-#{Time.now}”)
    12. Zip::ZipOutputStream.open(t.path) do |z|

    What am i doing wrong?

  6. Claudio scrive:

    Hi Ricardo,
    I’ve tried to reproduce your error without success.

    May be you’ve not included zip gems in your controller?

    Try with with:
    require ‘zip/zip’
    require ‘zip/zipfilesystem’

  7. Steve scrive:

    I’m getting all sorts of errors for this, namely that the images come out in the compressed folder corrupted, with bad filetypes and defintions.

    All of the zip creation and filenaming goes well, but when the file has been downloaded, the files just refuse to show due to either being corrupted (through ASCII conversion) or have bogus Huffman definitions which flag them as Qt-PICT exploits.

    Anyone else run into this issue?

  8. David scrive:

    Hey Steve, I am running into the same problem. I am on OS X running Ruby 1.9.1 .. whenever i try to unzip the resulting file Unarchiver tells me there is an error decrunching the file…

  9. saranya scrive:

    how to unzip an image file in rails3

  10. Andrew Kendall scrive:

    This worked great for me under 1.8.7 but after upgrading to Rails 3 and Ruby 1.9.2 I started running into the same problems as Steve. I suggest you give http://zipruby.rubyforge.org/ a try if you are having issues.

  11. Hamlet scrive:

    Hi,
    thanks for the tip about creating a zip file…
    But where did you see that Tempfile was creating a file in memory ?

    From the doc you linked :
    “Creates a temporary file of mode 0600 in the temporary directory”

    Otherwise it should fit your bill,
    Regards

  12. Simon scrive:

    Thanks for the article but I think it would be even better to call

    Time.now.to_i

    this gives you the timestamp and will be an integer. The Time.now you use could contain all kind of strange characters (e.g. a slash or dots) which may pose some problems on certain file systems, don’t you think?

  13. Jose scrive:

    With Ruby 1.9.2 I found using z.write instead of z.print fixed my zip corruption problem.

  14. Imelda Savary scrive:

    Tahnks for the interesting article!

  15. Koen scrive:

    this didn’t work for me on 1.8.7 in combination with heroku – i get an empty file – but when i closed the file and read it as data and streamed it as data with send_data than it does work.

    Zip::ZipOutputStream.open(file.path) do |z|
    files.each do |wood, report|
    title = wood.abbreviation+”.txt”
    z.put_next_entry(title)
    z.write report
    end
    end
    file.close
    file = File.open(file.path, “r”)
    data = file.read()
    send_data data, :type => ‘application/zip’, :disposition => ‘attachment’, :filename => file_name

  16. supernsetips scrive:

    Dude.. I am not much into reading, but somehow I got to read lots of articles on your website. Its amazing how interesting it is for me to visit you very often.

  17. Leda Stlaurent scrive:

    Very fantastic visual appeal on this site, I’d value it 10 10.

  18. Raphaël scrive:

    I’ve been using your code for an application coded in Rails 2.0.2 (Ruby 1.8.6).

    We had an issue that occurred very rarely: sometimes, during the send_file, the file was not found.

    After much investigation, we understood why: the send_file has an option (:stream) which defaults to true. If set to true, the file is read and sent on the fly. The problem is that in your code, you close and delete the temporary file just after the send_file.
    In our case, and maybe because we used an old version of Rails, the file was sometimes sent after being deleted (thus raising an error).

    The solution is just to add :stream => false:
    send_file t.path, :type => ‘application/zip’,
    :disposition => ‘attachment’,
    :filename => file_name,
    :stream => false
    t.close

    With this option, the file is read BEFORE being sent, and then (and only then) closed.

    This option disappeared in Rails 3 (or around it) because it is handled in another way that I haven’t investigated.
    I’d suggest you to add this for people who, like me, would come to your blog and experience the same problem :)

  19. [...] 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 [...]

  20. Edward Rudd scrive:

    A few things to note. Tempfile doesn’t create file in memory (unless your tmp directory is in something like tmpfs). Also you do not need to put Time.now or anything of the sort in the first parameter to the constructor. Tempfile.new takes a “basename” which is the prefix used to create the temporary filename.. the underlying make_tmpname method already mixes in the time and a random value.

  21. news scrive:

    If you dont mind, exactly where do you host your website? I am hunting for a great web host and your site seams to be quick and up most the time

  22. tahniyat scrive:

    Can we do this for remote files?

    For example, if i want to download and zip following photos:

    http://myApplication.s3.amazonaws.com/xxxxxxxx/image/image1.jpeg, http://myApplication.s3.amazonaws.com/xxxxxxxx/image/image2.jpeg, http://myApplication.s3.amazonaws.com/xxxxxxxx/image/image3.jpeg

    then what “path” i will set? Am i doing something which is not possible in rubyZip?

Leave a Reply

Insert code beetween <code lang="ruby"> and </code>

Copyright 2012 DevInterface s.n.c.

DevInterface Blog is proudly powered by WordPress