Imagine a situation where you have to check whether or not a user that sent the request can access or download files that were uploaded by another user. Perhaps user A uploaded a file that needs to be shared only with user B or only with authenticated users. If your application is deployed behind a reverse-proxy such as nginx, you can use the best of both worlds: your application for checking the user’s permissions and the Web server for serving the files the application tells it to serve.

Before we begin, there are a couple of things I would like to address. First of all, having your Web application serve the media files by loading it into memory and sending it in a response is grossly inefficient. You may not know the size of the file, or there could be many requests happening at once. Whatever the case may be, there is a better way.

X-Accel

To quote the official documentation:

X-accel allows for internal redirection to a location determined by a header returned from a backend.

This allows you to handle authentication, logging or whatever else you please in your backend and then have NGINX handle serving the contents from redirected location to the end user, thus freeing up the backend to handle other requests. This feature is commonly known as X-Sendfile.

To achieve this, at least two things have to be implemented:

  1. The application’s response must contain the X-Accel-Redirect header;
  2. The location should be marked as internal; to prevent direct access to the URI.

X-Accel-Redirect header

This header tells nginx which URI to serve. Although the following example uses django-rest-framework, the same thing can be achieved with any other Web framework.

If we assume all files uploaded by users are located in the /home/user/repo/media/ directory (also defined in Django’s MEDIA_ROOT setting), or more precisely, the /home/user/repo/media/files/{user.id}/ directory by the FileField’s upload_to function, the view looks something like this:

from pathlib import Path

from django.conf import settings
from django.http import HttpResponseRedirect

from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet

from .models import File
from .permissions import CanAccessFile

class FileViewSet(ModelViewSet):
    permission_classes = [CanAccessFile]
    queryset = File.objects.all()

    @action(detail=True, methods=["get"])
    def download(self, request, pk=None):
        obj = self.get_object()
        if settings.DEBUG:
            return HttpResponseRedirect(obj.upload.url)

        file_name = Path(obj.upload.path).name
        headers = {
            "Content-Disposition": f"attachment; filename={file_name}",
            "X-Accel-Redirect": (
                f"/uploads/files/{obj.user_id}/{file_name}"
            ),
        }
        return Response(data=b"", headers=headers)

Note that the settings.DEBUG block is here so developers can keep using Django’s static mechanism for serving media files during development.

There are also other X-Accel-* headers that can be set by the application to further refine the process.

internal

The application’s response that contains the X-Accel-Redirect header is picked up by the Web server on its way back to the client. In order for nginx to locate the file that should be sent to the client, the configuration should look something like this:

server {
    server_name example.com;

    location /favicon.ico { access_log off; log_not_found off; }
    location /static/ {
        root /home/user/repo;
    }

    location /uploads/ {
        internal;
        alias /home/user/repo/media/;
    }

    location / {
        include /etc/nginx/proxy_params;
        proxy_pass http://unix:/run/gunicorn.sock;
    }
}

With that all set, you’re ready to start serving files to select users!