Fix staged upload previews and allow WebP

- Staged files are now served through a signed URL (Django signing, 24h)
  so <img>/<video> tags can load previews without an Authorization
  header; the file endpoint accepts header auth or a valid signature,
  rejects tampered signatures, and still scopes access to the owner
- Serializer responses now carry the request context so URLs are signed
  per user
- Add .webp to the allowed extensions (backend + upload hint)
This commit is contained in:
2026-09-17 11:17:04 -05:00
parent d0e2901c92
commit 7deb6084b6
4 changed files with 64 additions and 9 deletions
+38 -5
View File
@@ -14,10 +14,13 @@ import shutil
from pathlib import Path
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core import signing
from django.http import Http404
from rest_framework import mixins, status, viewsets
from rest_framework.decorators import action
from rest_framework.parsers import FormParser, JSONParser, MultiPartParser
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from . import services
@@ -116,12 +119,42 @@ class TempUploadViewSet(
temp.file.delete(save=False)
temp.save()
return Response(
TempUploadSerializer(temp).data, status=status.HTTP_201_CREATED
self.get_serializer(temp).data, status=status.HTTP_201_CREATED
)
@action(detail=True, methods=["get"])
@action(detail=True, methods=["get", "head"], permission_classes=[AllowAny])
def file(self, request, pk=None):
temp = self.get_object()
"""Serve the staged file; accepts a signed URL for media tags."""
user = request.user if request.user.is_authenticated else None
if user is None:
signature = request.query_params.get("sig")
if signature:
try:
payload = signing.loads(
signature,
salt=services.UPLOAD_FILE_SALT,
max_age=86400,
)
except signing.BadSignature:
payload = None
if payload and str(payload.get("temp")) == str(pk):
user = (
get_user_model()
.objects.filter(pk=payload.get("user"))
.first()
)
if user is None or not user.can_upload:
return Response(
{"detail": "Authentication required."},
status=status.HTTP_401_UNAUTHORIZED,
)
temp = TempUpload.objects.filter(pk=pk).first()
is_owner = temp is not None and temp.user_id == user.id
if temp is None or not (
is_owner or user.is_staff or user.is_superuser
):
raise Http404
if not temp.file:
raise Http404
return services.serve_file(request, temp.file.path)
@@ -139,7 +172,7 @@ class TempUploadViewSet(
if temp.status == TempUpload.STATUS_PENDING and temp.iqdb_data:
temp.status = TempUpload.STATUS_VISUAL_MATCH
temp.save(update_fields=["iqdb_data", "status", "updated_at"])
return Response(TempUploadSerializer(temp).data)
return Response(self.get_serializer(temp).data)
@action(detail=True, methods=["post"])
def resolve(self, request, pk=None):
@@ -204,7 +237,7 @@ class TempUploadViewSet(
status=status.HTTP_400_BAD_REQUEST,
)
temp.refresh_from_db()
return Response(TempUploadSerializer(temp).data)
return Response(self.get_serializer(temp).data)
def perform_destroy(self, instance):
if instance.file: