Using Nginx to send files with x-accel-redirect

So far I’ve configured Nginx to handle file uploads by caching the file to disk and telling rails where it is, rather than passing it through in the request, not fun with large files.  Now to do the reverse.  Instead of Rails sending files to users thru Nginx, Rails can tell Nginx what file to send.

I’d initially assumed that when a Rails application (lets say Mongrel) was sending a file with the ’send_file’ method then it couldn’t handle other requests as they came in.  Seeing as that was an assumption rather than fact I setup a download action on my Ubuntu dev server to show this happening.  Basically a website supported by a single Mongrel - if it’s busy sending a large file additional requests will get queued up eventually giving 503 errors I figured as they timed out.

I set the action going to download a 40mb test file on the server, opened up another tab and loaded another test page and it appear straight away.  So my assumption was wrong, but not completely wrong it turns out.  Mongrel is still sending the file though as if I kill it the download terminates.  This is because Mongrel has it’s own internal request queue and only sends one request at a time to your Rails code. Hence the ability to handle additional requests while someone is downloading a file and websites running clusters of Mongrels.  Getting Mongrel to send the file isn’t the best use of resources though.

For Nginx to send files on Mongrel’s behalf two changes are needed.  Firstly you need to tell Nginx that it should be doing this and from where.  The Nginx sendfile page is quite helpful in this regard, you’ll end up having something like this:

location /files/ {
  internal;
  root
/;  # note the trailing slash
}

Note, sendfile is enabled by default in all nginx.conf files I’ve seen.

Secondly, in your Rails download action do something like this:

if
  head(:x_accel_redirect => "/files/#{filename}",  :content_type => File.mime_type?(file), :content_disposition => "attachment; filename=#{filename}")
else
  send_file("#{RAILS_ROOT}/files/#{filename}", :type => File.mime_type?(file))
end

It is important that you specify the correct mime_type to stop the receiving browser guessing and potentially changing the file extension.  If one of the standard rails attachment gems is being used, then you’ll likely have that information already.  But if you don’t, like in the example above, then mimetype_fu is a very handy plugin as it extends the File class by adding the mime_type? method.

If you store files with guid like names, then the file name received by the user can be controlled by changing the :content_disposition value.

Finally, the reason I’ve specified the source tree root is so that files from multiple top level folders can be accessed from the one location entry in the Nginx config.

There is a plugin somewhere that does the head changes for you, but it’s been abandoned and even has a message suggesting that it shouldn’t be used for that very reason.  I’ve not used it for that reason and because it assumes all file downloads are to be handled by Nginx.  That’s a nasty assumption that would bite someone one day, so not very fun.  So I’m in the process of rolling my own.

Apache can do the same with with mod_xsendfile.

Related posts:

  1. Nginx upload awesomeness I’ve been a bit busy earning my keep for the...

This entry was posted in Nginx File Transfers and tagged , , , , , . Bookmark the permalink. Post a comment or leave a trackback: Trackback URL.

Post a Comment

Your email is never published nor shared. Required fields are marked *

*
*
  •  

  • About Nahum Wild

    I'm a High Performance Website Consultant specialising in Ruby on Rails deployments. In this blog I cover common problems I've seen and provide insight on optimisation techniques.

  • Recommend Me

    Follow me on Twitter