Easy zip generating with Rails, Nginx & Paperclip
Posted on · · Tags: paperclip
Tonight I decided to try making NGINX handle my zip generating, to save the server some work, and the user of some frustration waiting for background jobs to finish. Here’s what I came up with.
First of all, I re-compiled Nginx with two modules; mod_zip, and passenger module. You may drop the Passenger module if you are using some other server, this solution works with anything that can print out some text :)
Roughly, these are the required steps:
gem install passenger
cd nginx-folder/
.configure --add-module=/path/to/passenger/gem/ext/nginx --add-module=/path/to/mod_zip-1.x
sudo make && sudo make install
If you wonder where your passenger is, you can easily find the root of passenger by running
passenger-config --root
So on to the fun part! I started by adding a member method to my galleries route called zip:
ActionController::Routing::Routes.draw do |map|
map.resources :galleries, :member => {:zip => :get}
end
And In my controller I added these two methods (it can probably be done cleaner, but hey, it’s working :) Suggestions for other ways of writing these are welcome!
class GalleriesController < ApplicationController
def zip
@gallery = Gallery.find(params[:id])
paperzip(@gallery.images.collect(&:data), @gallery.name)
end
private
def paperzip( attachments= [], name= 'file', style= :original )
# set headers
response.headers["Content-Disposition"] = "attachment; filename=#{name}.zip"
response.headers["X-Archive-Files"] = "zip"
# Generate file list
render :text => attachments.collect{|a|
['-', File.size(a.path(style)), a.url(style,false), a.original_filename.gsub(/ /,'_')].join(' ')
}.join("\r\n")+"\r\n"
end
end
Whoa! Let me break that down for you a little bit. Firstly the member method zip, which now has the named route “zip_gallery_path(@gallery)” in you views btw:
def zip
@gallery = Gallery.find(params[:id])
paperzip(@gallery.images.collect(&:data), @gallery.name)
end
This basically finds our gallery from the :id param, and calls on this method we’re defining as private for this controller, called paperzip() (I know, catchy). paperzip() takes a collection of Images (which has_attached_file :data), a string containing a name for the zip file about to be made, and a symbol which you can use to zip down the style you want. I’m running with :original here.
Let’s move on to the next couple of lines:
response.headers["Content-Disposition"] = "attachment; filename=#{name}.zip"
response.headers["X-Archive-Files"] = "zip"
What we do here is setting some custom headers for our response back to the client, and Nginx. The content-disposition tells it we want the user to be prompted to download a file, called something.zip. The X-Archive-Files header is the one that makes mod_zip trigger, and eagerly await a list of files to zip down and send. Mod_zip requires a list of files that looks something like this:
md5 bytes /relative/path filename<linebreak>
Note that the linebreak needs to be a \r\n, and be on every line, even the last one. So, in order to construct this list, I added the following lines to the method, returning it using “render :text”
render :text => attachments.collect{|a|
['-', File.size(a.path(style)), a.url(style,false), a.original_filename.gsub(/ /,'_')].join(' ')
}.join("\r\n")+"\r\n"
And voila! Calling this action with a get request, will now trigger mod_zip and create us a fully working zip file. The good thing about using this over some other method, is that Nginx only uses a couple of kilobytes of that precious memory of yours while doing this, and does the job way quicker than any other method I’ve tried.