diff --git a/Dockerfile b/Dockerfile
index af92f661662d08edf79e5615f12833aeb0d34f13..0ac59cabd4109dd15a888832ce38b284c415af58 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -6,7 +6,7 @@ FROM alpine:3.13.4
ENV PYTHONUNBUFFERED 1
ENV BUILDPKGS gettext py3-pip py3-wheel
-ENV RUNTIMEPKGS uwsgi uwsgi-python python3 py3-psycopg2 py3-pyldap libmagic proj gdal
+ENV RUNTIMEPKGS uwsgi uwsgi-python uwsgi-router_static python3 py3-psycopg2 py3-pyldap libmagic proj gdal
RUN adduser -D -g '' uwsgi
diff --git a/castellum/settings/default.py b/castellum/settings/default.py
index 7a26d7db3d0619f826cdb05a50deea9fc0889fb6..e553c6513c027cf2a9c51df905638fbef2190470 100644
--- a/castellum/settings/default.py
+++ b/castellum/settings/default.py
@@ -180,6 +180,23 @@ MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
+# By default, castellum does not serve media (aka "uploaded") files.
+# You have 3 options here:
+#
+# - Set PROTECTED_MEDIA_SERVER to "django" (only for development)
+# - Set PROTECTED_MEDIA_SERVER to None and let a proxy serve the files
+# instead. This is insecure because it bypasses castellum login.
+# - Set PROTECTED_MEDIA_SERVER to the proxy server you use (e.g.
+# 'nginx', 'uwsgi') to use X-Sendfile. You will have to configure
+# the proxy accordingly. This option is strongly recommended.
+#
+# references:
+# - https://docs.djangoproject.com/en/3.2/ref/contrib/staticfiles/#django.contrib.staticfiles.views.serve
+# - https://www.nginx.com/resources/wiki/start/topics/examples/xsendfile/
+# - https://uwsgi-docs.readthedocs.io/en/latest/Snippets.html#x-sendfile-emulation
+PROTECTED_MEDIA_SERVER = None
+PROTECTED_MEDIA_URL = '/protected/'
+
NPM_ROOT_PATH = BASE_DIR.parent
NPM_FILE_PATTERNS = {
diff --git a/castellum/settings/development.py b/castellum/settings/development.py
index 64f07f12d576e00c63388c169cd1205006a186a3..aa1c789277d5374cf8ebeec0ea8aabc596c8681c 100644
--- a/castellum/settings/development.py
+++ b/castellum/settings/development.py
@@ -7,6 +7,7 @@ SESSION_COOKIE_SECURE = False
DEBUG = True
+PROTECTED_MEDIA_SERVER = 'django'
# setup debug toolbar
diff --git a/castellum/urls.py b/castellum/urls.py
index a3a26c2de879deb2c9f27949d15a41d545d9e31e..d47bdcd57251c4d8be540907e0853241ed64a1d9 100644
--- a/castellum/urls.py
+++ b/castellum/urls.py
@@ -19,8 +19,11 @@
# License along with Castellum. If not, see
# .
+import mimetypes
+
from django.conf import settings
from django.contrib import admin
+from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.views import LoginView
from django.contrib.auth.views import LogoutView
@@ -54,6 +57,24 @@ class FeedsView(LoginRequiredMixin, TemplateView):
return context
+@login_required
+def protected_media(request, path):
+ if settings.PROTECTED_MEDIA_SERVER == 'django':
+ return serve(request, path, document_root=settings.MEDIA_ROOT)
+ elif settings.PROTECTED_MEDIA_SERVER == 'nginx':
+ response = HttpResponse()
+ response['X-Accel-Redirect'] = settings.PROTECTED_MEDIA_URL + path
+ return response
+ else:
+ mimetype, encoding = mimetypes.guess_type(settings.MEDIA_ROOT / path)
+ response = HttpResponse()
+ response['Content-Type'] = mimetype
+ if encoding:
+ response['Content-Encoding'] = encoding
+ response['X-Sendfile'] = settings.MEDIA_ROOT / path
+ return response
+
+
def dummy(request):
return HttpResponse('', status=204)
@@ -84,9 +105,9 @@ urlpatterns = [
),
]
-if settings.DEBUG:
+if settings.PROTECTED_MEDIA_SERVER:
urlpatterns += [
- path('media/', serve, {'document_root': settings.MEDIA_ROOT}),
+ path('media/', protected_media),
]
try:
diff --git a/docs/example_deployment/settings.py b/docs/example_deployment/settings.py
index c67bc88c5b26b84ff5e3cfec7fad62e3d6e55e68..cf6fcb87c63f1f5e150954de008bcf54422ce4a6 100644
--- a/docs/example_deployment/settings.py
+++ b/docs/example_deployment/settings.py
@@ -28,7 +28,8 @@ DATABASES = {
ALLOWED_HOSTS = ['*']
-MEDIA_ROOT = '/media'
+MEDIA_ROOT = Path('/media')
+PROTECTED_MEDIA_SERVER = 'uwsgi'
# The example deployment does not contain a mail server, so use dummy instead
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
diff --git a/docs/example_deployment/uwsgi.ini b/docs/example_deployment/uwsgi.ini
index d9f1e02a53952fcdc65213a4847ad620b6164c1d..1127b3637a815aadc335716e7daf9107bf38e3b2 100644
--- a/docs/example_deployment/uwsgi.ini
+++ b/docs/example_deployment/uwsgi.ini
@@ -9,8 +9,14 @@ socket=0.0.0.0:8000
# enables internal http server and router
protocol=http
static-map=/static=/code/castellum/collected_static
-static-map=/media=/media
static-map=/favicon.ico=/code/castellum/collected_static/images/favicon.ico
+# https://uwsgi-docs.readthedocs.io/en/latest/Snippets.html#x-sendfile-emulation
+plugins = router_static
+offload-threads = 2
+static-safe = /media
+collect-header = X-Sendfile X_SENDFILE
+response-route-if-not = empty:${X_SENDFILE} static:${X_SENDFILE}
+
wsgi-file=castellum/wsgi.py
plugin=python