Generating ZIP file archives via Ruby on Rails
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.
Thanks man!
I was trying out zlib / gzip but kept ending up with a corrupted archive. rubyzip is so much easier to use! :D
Forgot to add, I don’t think the statement “require ‘zip/zipfilesystem’” is needed :).
No problem, Keng. Glad you got some use out of this!
[...] 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. [...]
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!
[...] superwick.com » Generating ZIP file archives via Ruby on Rails (tags: zip rails ruby howto development) [...]
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!