Rails#send_file + Nginx X-Accel-Redirect
Sometimes you may need to serve some static files (CSV, PDF, XLS etc) to your users, but only after they have logged in. Obviously you can’t just keep the static file in your public folder as anyone could just use the URL to download files.
One possible solution for protected downloads is to just use the #send_file method provided by Rack to send a non-public file to the user, but serving static files with your app server (Unicorn, Mongrel, Thin etc) is a bad idea as it’s really inefficient. The best approach is to allow the app server to handle the authentication/authorization and then hand the actual downloading to your web server (Nginx, Apache, Lighttpd etc).
In Lighttpd server it can be done by returning X-Sendfile header from your script. Nginx have its own implementation of such idea using X-Accel-Redirect header.
The need for X-Accel-Redirect:
- To deliver large files.
- For those files to not be available to the public
- Would be able to free some resources on server while nginx will handle all slow requests to dynamic content
In this article I will assume that the site is located in /home/kranjith/sites/projects/blog directory and there are some static files (like CSV, PDF, XLS etc) located in /home/kranjith/sites/projects/blog/uploads directory.
First of all, lets take a look at our nginx configuration:
# Nginx X-Accel-Redirect configuration for Rails and Unicorn upstream unicorn_blog_server { server unix:/home/kranjith/sites/projects/blog/tmp/sockets/unicorn.sock fail_timeout=0; } server { listen 80; server_name example.org; root /home/kranjith/sites/projects/blog/public; location /downloads { internal; alias /home/kranjith/sites/projects/blog/uploads; } location / { try_files $uri @blog; } location @blog { proxy_redirect off; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Sendfile-Type X-Accel-Redirect; proxy_set_header X-Accel-Mapping /home/kranjith/sites/projects/blog/uploads/=/downloads/; proxy_pass http://unicorn_blog_server; } }
The internal keyword for the /downloads location prevents the uploads folder from being publicly accessible.
Next you need to ensure that Rails knows what server you are using
In config/environments/production.rb file.
# Specifies the header that your server uses for sending files. config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx
In DownloadsController, just do whatever authorization you need to, then use #send_file to serve the file to the user:
class DownloadsController < ApplicationController load_and_authorize_resource def show send_file( Rails.root.to_s + @uploaded_file.file.url, type: @uploaded_file.content_type, filename: @uploaded_file.filename, dispostion: "inline", status: 200, stream: true, x_sendfile: true ) end end
I am using CarrierWave to upload files from Rails applications. In config/initializers/carrierwave.rb
CarrierWave.configure do |config| config.storage = :file config.root = Rails.root config.store_dir = "uploads" end
And that’s it! With described approach we are able to create very flexible and extremely performance systems for file distribution!
Now I’m going to run through a specific example of downloading a file.
1.Browser makes a request for a file
# HTTP Headers GET /downloads/SecretSquirrel.zip
2.Nginx receives this request. It adds on a header with configuration data that will be required by rails.
# Example HTTP Headers with additional header added by nginx GET /downloads/SecretSquirrel.zip X-Accel-Mapping: /home/kranjith/sites/projects/blog/uploads/=/downloads/
3.Nginx passes the request onto Rails and it invokes the relevant controller.
4.The controller makes its authorization checks and calls send_file. Use the absolute path to the file.
# controller code (e.g. app/controllers/downloads_controller.rb) send_file('/home/kranjith/sites/projects/blog/uploads/SecretSquirrel.zip')
5.Rails (Rack to be precise) then decides what to with the file. Rails knows what server we are using (from config/environments/production.rb). Instead of using the file as the body of the request, it will add a header to the response. It uses the X-Accel-Mapping that nginx added earlier to change the file path.
# HTTP Response HTTP/1.1 200 OK X-Accel-Redirect: /downloads/SecretSquirrel.zip Content-Type: application/octet-stream Content-length: ... Content-Disposition: attachment; filename="SecretSquirrel.zip" <empty body>
6.Nginx receives this header from rails and interprets it. It finds the location directive and reverses the changes to the path that rails made in step 5.
# HTTP Response HTTP/1.1 200 OK Content-Type: application/octet-stream Content-Length: ... Content-Disposition: attachment; filename="SecretSquirrel.zip" <contents of /home/kranjith/sites/projects/blog/uploads/SecretSquirrel.zip>
7.Browser receives the file as if it was a normal download.