June 14th, 2007 by Chad

Today I was presented with a challenge. In a project I’ve been working on (to be announced soon)the Matt Sommers Digital Archive, there are musical artists, with many albums, which have multiple tracks. In the app, the tracks are uploaded via a form, and are processed using attachment_fu. What we needed was a way to generate a zip file of an entire album’s mp3’s, with the ability to re-generate it if any of the tracks change, and be able to present a link to make the entire album downloadable. At first, it sounded a little hard to do, but once I found out there was something called rubyzip, it became easy as cake. Here’s how I did it. (Hat tip to the author of this post for helping make it even easier for me.)

So, to begin, I installed the rubyzip gem:

gem install rubyzip

Then, in the model that I’m using to generate the zip bundles, I add a couple “require” statements:

require 'zip/zip'
require 'zip/zipfilesystem'

class Album < ActiveRecord::Base
  (...)
end

Next, I added an instance method called bundle, which when called will use rubygem to generate the zip file. Note: the “permalink” attributes of Album and Artist are populated when an object of those models is created. I’m using them because it makes for nice filenames, too.

# create a zipped archive file of all the tracks in an album
def bundle(name = self.permalink, set = self.artist.permalink)
   bundle_filename = "#{RAILS_ROOT}/public/uploads/#{set}-#{name}.zip"

   # check to see if the file exists already, and if it does, delete it.
   if File.file?(bundle_filename)
     File.delete(bundle_filename)
   end 

   # set the bundle_filename attribute of this object
   self.bundle_filename = "/uploads/#{set}-#{name}.zip"

   # open or create the zip file
   Zip::ZipFile.open(bundle_filename, Zip::ZipFile::CREATE) {
     |zipfile|
     # collect the album's tracks
     self.tracks.collect {
       |track|
         # add each track to the archive, names using the track's attributes
         zipfile.add( "#{set}/#{track.num}-#{track.filename}", "#{RAILS_ROOT}/public#{track.public_filename}")
       }
   }

   # set read permissions on the file
   File.chmod(0644, bundle_filename)

   # save the object
   self.save
end
  

Next I added a method in my controller:

def create_bundle
   album = Album.find(params[:id])
   album.bundle
   flash[:notice] = 'Album was successfully zipped.'
   redirect_to album_url(album.artist, album)
end

And edit my routes.rb accordingly:

map.create_bundle 'create_bundle/:id', :controller => 'albums', :action => 'create_bundle'

Now it’s just a matter of creating a link in the view for the admin to click whenever he/she wants to generate the zip file:

<%= link_to('Create Album Zip', create_bundle_path(@album)) %>

…and a link for the user to click to download the zip file if it exists:

<% unless @album.bundle_filename.nil? %>
  <div id="grid_right">
    <h2><%= link_to "Download Album Zip", @album.bundle_filename %></h2>
  </div>
<% end %>

That’s it! Refer to the rubyzip documentation for more goodness.

Hit me up on Twitter, Facebook, Flickr, or comment below!

9 Responses to “Generating ZIP file archives via Ruby on Rails”

  1. Thanks man!

    I was trying out zlib / gzip but kept ending up with a corrupted archive. rubyzip is so much easier to use! :D

  2. Forgot to add, I don’t think the statement “require ‘zip/zipfilesystem’” is needed :).

  3. No problem, Keng. Glad you got some use out of this!

  4. [...] Generating ZIP file archives via Ruby on Rails – This pointed me in the right direction to satisfy another client requirement. Always nice when that happens. [...]

  5. Nicely done. I’m working on a very (very!) similar project and I spent all night figuring this out myself only to find this page after I finished. You code is much cleaner than mine but our approaches are nearly identical. (My zip file is made on the after_save call for the Album class and I include cover art and liner notes as well. ) The design on your archive site is very elegant as well. I’m still prototyping with ActiveScaffold. You’ve set the bar high!

    Cheers!

  6. [...] superwick.com » Generating ZIP file archives via Ruby on Rails (tags: zip rails ruby howto development) [...]

  7. Great tutorial! This came in really handy. One semi-unrelated note: File.join will deal with a leading ‘/’ (or lack thereof) for your filename, so instead of:

    “#{RAILS_ROOT}/public#{track.public_filename}”

    A more robust solution might be:

    File.join RAILS_ROOT, ‘public’, track.public_filename

    Other than that, extremely useful and extremely elegant. Thank you!

  8. “Next, I added a class method…”

    should probably be:

    “Next, I added an instance method…”

  9. Thanks,

    You save my hours :-)

    Cheers,

Leave a Reply