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 a class 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.

7 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!

Leave a Reply