Skip to content

Instantly share code, notes, and snippets.

@cobusc
Last active August 31, 2021 17:32
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save cobusc/ea1d01611ef05dacb0f33307e292abf4 to your computer and use it in GitHub Desktop.
Save cobusc/ea1d01611ef05dacb0f33307e292abf4 to your computer and use it in GitHub Desktop.
Protected Media in Django

Protected Media in Django

Introduction

Django manages media based on the following definitions:

BASE_DIR = /var/praekelt/telkom-spliceworks/
MEDIA_ROOT = "%s/media/" % BASE_DIR
MEDIA_URL = "/media/"

File fields are typically defined as:

    upload = models.FileField(upload_to="uploads/")
    # File will be stored under MEDIA_ROOT + upload_to

In a typical production environment one would let nginx serve the media:

# Publicly accessible media
location ^~ /media/ {
    alias /var/praekelt/media/
}

This works well when the media should be publically accessible. However, if the media should be protected, we need a way for Django to check whether the request for the media should only be allowed for logged in (or more stringent criteria) users.

Proposed Solution

The proposed solution consists of:

  • new settings.py attributes,
  • a customized FileSystemStorage class,
  • a custom handler for the protected media URL and
  • additional nginx configuration.

To support protected media, it need to be stored on a different physical location to publically accessible media. It is proposed that the following new attributes be defined in settings.py:

PROTECTED_MEDIA_ROOT = "%s/protected/"
PROTECTED_MEDIA_URL = "/protected"
PROTECTED_MEDIA_LOCATION_PREFIX = "/internal"  # Prefix used in nginx config

When defining a file or image field that needs to be protected, we use a custom storage class ProtectedFileSystemStorage, which will use the new settings attributes:

class ProtectedFileSystemStorage(django.core.storage.FileSystemStorage):
    @cached_property
    def base_location(self):
        return self._value_or_setting(self._location, settings.PROTECTED_MEDIA_ROOT)

    @cached_property
    def base_url(self):
        if self._base_url is not None and not self._base_url.endswith('/'):
            self._base_url += '/'
        return self._value_or_setting(self._base_url, settings.PROTECTED_MEDIA_URL)

Two custom field classes will expose this:

class ProtectedFileField(FileField):
    def __init__(self, **kwargs):
        if "storage" in kwargs:
            raise Exception()
        super(ProtectedFileField, self).__init__(storage=ProtectedFileSystemStorage, **kwargs)

class ProtectedImageField(ImageField):
    def __init__(self, **kwargs):
        if "storage" in kwargs:
            raise Exception()
        super(ProtectedFileField, self).__init__(storage=ProtectedFileSystemStorage, **kwargs)

These can be used as follows:

    upload = models.ProtectedFileField(upload_to="uploads/")
    # File will be stored to PROTECTED_MEDIA_ROOT + upload_to

This will need a custom handler for the URL, which needs the appropriate nginx config.

@login_required
def protected_view(request, path):
    """
    Redirect the request to the path used by nginx for protected media.
    """
    response = HttpResponse()
    response["X-Accel-Redirect"] = os.path.join(
        settings.PROTECTED_MEDIA_LOCATION_PREFIX, path
    )
    return response

In urls.py, add:

if settings.DEBUG:
    urlpatterns += [
        url(
            r"^{}/(?P<path>.*)$".format(settings.MEDIA_URL),
            "django.views.static.serve",
            {"document_root": settings.MEDIA_ROOT, "show_indexes": True}
        ),
        url(
            r"^{}/(?P<path>.*)$".format(settings.PROTECTED_MEDIA_URL), 
            "django.views.static.serve",
            {"document_root": settings.PROTECTED_MEDIA_ROOT, "show_indexes": True}
        ),
    ]
else:
    urlpatterns += [
        url(
            r"^{}/(?P<path>.*)$".format(settings.PROTECTED_MEDIA_URL), 
            protected_view
        ),
    ]

The updated nginx configuration:

# Protected media
location ^~ /internal/ {
    internal;  # Cannot be access from external calls
    alias /var/praekelt/protected/
}

# Publicly accessible media
location ^~ /media/ {
    alias /var/praekelt/media/
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment