Serving Files with Flask behind nginx & gunicorn

I am building a social media data collection and analysis web application DD-CSS where at some point users download csv or json files. I will discuss this project later, the subject of this post is how to let users download files. First, let me introduce the technologies on the server:

  • Flask is a micro-framework for web development in Python
  • Gunicorn is a standalone WSGI container/HTTP Server
  • Nginx is a HTTP server and reverse proxy (we use it for serving the static pages and for proxying our upstream server Gunicorn)

Configurations

Before implementing the download functionality I would like to mention how I configure and run gunicorn and nginx. Here is a list of deployment options of a Flask application object (a WSGI application). Gunicorn is a python package so we install it with pip to our virtualenv.
pip install gunicorn
When running it, we can set the number of workers, path to log-file (in this case, stdout), network accessibility (host and port), etc. such as
gunicorn -w 4 --log-file=- --bind=0.0.0.0:8000 manage:app

Configuring Nginx

yum install nginx //yum, apt-get, brew, etc. you got it...
nginx installation completes with an information like this:
The default port has been set in /usr/local/etc/nginx/nginx.conf to 8080 so that
nginx can run without sudo.

To have a better understanding of the configuration file please head over to the beginner’s guide.

Now instead of modifying the default nginx.conf file we create sites-available and site-enabled directories and ensure to include the site-specific configurations by having include statement in the core http module in etc/nginx/nginx.conf file:
include /usr/local/etc/nginx/sites-enabled/*;

Then, following these three posts (see thisthat and the other):

server {
    listen 8090;
    server_name localhost;
 
    root /Users/toz/Documents/workspace/dd-css/;
 
    access_log /Users/toz/Documents/workspace/dd-css/logs/access.log;
    error_log /Users/toz/Documents/workspace/dd-css/logs/error.log;
 
    location /download/ {
        internal; 
        root /Users/toz/Documents/workspace/dd-css/;
    }

    location / {
        proxy_set_header X-Sendfile-Type X-Accel-Redirect;
        proxy_set_header X-Accel-Mapping /Users/toz/Documents/workspace/dd-css/=/download/;

        proxy_set_header X-Forward-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;
        if (!-f $request_filename) {
            proxy_pass http://127.0.0.1:5000;
            break;
        }
    }
   
    location /docs {
        root /Users/toz/Documents/workspace/dd-css;
        access_log off;
    }
}

/usr/local/etc/nginx/sites-available/dd-css.conf (END)

Note: Beware of selinux settings. On CentOS Nginx returns a 403 although all permissions are set properly. Here is how to fix this.

On the Flask side

Template:

<a href={{ url_for('main.download',file_id= query._id) }} class="mega-octicon octicon-cloud-download"></a>

Here is the redirector function:

@main.route('/query/<file_id>')
def download(file_id):
    (file_basename, server_path, file_size) = get_file_params(file_id)
    response = make_response()
    response.headers['Content-Description'] = 'File Transfer'
    response.headers['Cache-Control'] = 'no-cache'
    response.headers['Content-Type'] = 'application/octet-stream'
    response.headers['Content-Disposition'] = 'attachment; filename=%s' % file_basename
    response.headers['Content-Length'] = file_size
    response.headers['X-Accel-Redirect'] = server_path # nginx: http://wiki.nginx.org/NginxXSendfile
    return response

and the helper function:

def get_file_params(filename):
    filepath = os.path.abspath(current_app.root_path)+"/../download/"+filename
    if os.path.isfile(filepath):
        return filename,"/download/"+filename,os.path.getsize(filepath)
    with open(filepath, 'w') as outfile:
        data = load_from_mongo("ddcss","queries",\
            criteria = {"_id" : ObjectId(filename)}, projection = {'_id': 0}) 
        #outfile.write(json.dumps(data[0], default=json_util.default))
        outfile.write(dumps(data[0])) 
    return filename, "/download/"+filename, os.path.getsize(filepath)