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.
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/
}