From 553ac9abf19794fddcf0a0218977a8fd77a912fb Mon Sep 17 00:00:00 2001 From: Simonas Kareiva Date: Sun, 17 May 2026 12:38:58 +0300 Subject: [PATCH] Enable stl download --- apps/submissions/views.py | 44 +++++++++++++++++++++++++++++++++ hamprint/settings/base.py | 2 +- hamprint/urls.py | 6 +++++ templates/dashboard/detail.html | 23 ++++++++++++----- 4 files changed, 68 insertions(+), 7 deletions(-) diff --git a/apps/submissions/views.py b/apps/submissions/views.py index d8d2e3e..4797ee0 100644 --- a/apps/submissions/views.py +++ b/apps/submissions/views.py @@ -18,9 +18,11 @@ import secrets from django.contrib import messages from django.db import IntegrityError, transaction +from django.http import FileResponse, Http404 from django.urls import reverse, reverse_lazy from django.utils import timezone from django.utils.html import format_html +from django.views.decorators.http import require_safe from django.views.generic import CreateView from .emails import send_confirmation_email @@ -28,6 +30,48 @@ from .forms import SubmissionForm from .models import Submission, VerifiedEmail +@require_safe +def serve_stl(request, path: str): + """Auth-checked passthrough to a Submission's uploaded `.stl`. URL is + `/media/` so it matches `MEDIA_URL` -- which means + `{{ submission.stl_file.url }}` in templates Just Works. + + `path` is the relative name Django stored (e.g. `stl/foo.stl`); we + look the row up by that string rather than reading the filesystem + directly so an attacker can't escape `MEDIA_ROOT` with `..` segments. + + Access tiers match `SubmissionDetailView` (plan.md ยง7.4 step 7): + + - staff: yes (operators need the file to drive the printer). + - authenticated owner (`submitted_by == request.user`): yes. + - everyone else: 404 -- we deliberately don't 403 so the existence + of a row isn't leaked to non-owners by status code. + + Guests can't authenticate by design, so they can't pull their own STL + via this path. That's consistent with the detail page's two-tier + access; the submitter has the file on their own machine in any case. + """ + sub = Submission.objects.filter(stl_file=path).first() + if sub is None: + raise Http404 + user = request.user + is_owner = ( + user.is_authenticated + and sub.submitted_by_id is not None + and sub.submitted_by_id == user.id + ) + if not (user.is_staff or is_owner): + raise Http404 + # `FieldFile.open("rb")` is rooted in MEDIA_ROOT; FileResponse handles + # streaming, range requests, and closing the handle for us. + filename = sub.stl_file.name.rsplit("/", 1)[-1] + return FileResponse( + sub.stl_file.open("rb"), + as_attachment=True, + filename=filename, + ) + + class SubmitView(CreateView): """Public submit form. GET renders, POST creates a Submission.""" diff --git a/hamprint/settings/base.py b/hamprint/settings/base.py index edcdf88..96abbde 100644 --- a/hamprint/settings/base.py +++ b/hamprint/settings/base.py @@ -107,7 +107,7 @@ STATIC_URL = "static/" STATIC_ROOT = BASE_DIR / "staticfiles" STATICFILES_DIRS = [BASE_DIR / "static"] if (BASE_DIR / "static").exists() else [] -MEDIA_URL = "media/" +MEDIA_URL = "/media/" MEDIA_ROOT = BASE_DIR / "media" STORAGES = { diff --git a/hamprint/urls.py b/hamprint/urls.py index f20419d..23cbf56 100644 --- a/hamprint/urls.py +++ b/hamprint/urls.py @@ -18,6 +18,8 @@ Including another URLconf from django.contrib import admin from django.urls import include, path +from apps.submissions.views import serve_stl + urlpatterns = [ path("admin/", admin.site.urls), # Our local /accounts/ routes (close-account, etc.) come BEFORE allauth's @@ -25,6 +27,10 @@ urlpatterns = [ # through to allauth. path("accounts/", include("apps.accounts.urls")), path("accounts/", include("allauth.urls")), + # Auth-checked media serve. Lives here (not in apps.submissions.urls) + # because the prefix is MEDIA_URL, not /submit/. WhiteNoise serves + # /static/ only, so this is the sole handler for /media/. + path("media/", serve_stl, name="serve_stl"), path("", include("apps.dashboard.urls")), path("submit/", include("apps.submissions.urls")), ] diff --git a/templates/dashboard/detail.html b/templates/dashboard/detail.html index 4854b81..70d2d6f 100644 --- a/templates/dashboard/detail.html +++ b/templates/dashboard/detail.html @@ -136,13 +136,24 @@

Source

{% if submission.source_type == 'upload' %} -
- -
-

{% if submission.stl_file %}{{ submission.stl_file.name }}{% else %}(file not on disk){% endif %}

-

Raw .stl upload

+ {% if submission.stl_file %} + + +
+

{{ submission.stl_file.name }}

+

Raw .stl upload · click to download

+
+ +
+ {% else %} +
+ +
+

(file not on disk)

+

Raw .stl upload

+
-
+ {% endif %} {% else %} {{ submission.source_type|slice:":1"|upper }}