API: File upload infrastructure

This commit is contained in:
Raphael Michel
2021-01-05 12:49:58 +01:00
parent 9d70fd675c
commit 8b08b43e77
6 changed files with 149 additions and 1 deletions

View File

@@ -8,7 +8,7 @@ from pretix.api.views import cart
from .views import (
checkin, device, event, exporters, item, oauth, order, organizer, user,
version, voucher, waitinglist, webhooks,
upload, version, voucher, waitinglist, webhooks,
)
router = routers.DefaultRouter()
@@ -95,6 +95,7 @@ urlpatterns = [
url(r"^device/roll$", device.RollKeyView.as_view(), name="device.roll"),
url(r"^device/revoke$", device.RevokeKeyView.as_view(), name="device.revoke"),
url(r"^device/eventselection$", device.EventSelectionView.as_view(), name="device.eventselection"),
url(r"^upload$", upload.UploadView.as_view(), name="user.me"),
url(r"^me$", user.MeView.as_view(), name="user.me"),
url(r"^version$", version.VersionView.as_view(), name="version"),
]

View File

@@ -0,0 +1,53 @@
import datetime
from django.utils.timezone import now
from oauth2_provider.contrib.rest_framework import OAuth2Authentication
from rest_framework.authentication import SessionAuthentication
from rest_framework.exceptions import ValidationError
from rest_framework.parsers import FileUploadParser
from rest_framework.response import Response
from rest_framework.views import APIView
from pretix.api.auth.device import DeviceTokenAuthentication
from pretix.api.auth.token import TeamTokenAuthentication
from pretix.base.models import CachedFile
ALLOWED_TYPES = {
'image/gif': {'.gif'},
'image/jpeg': {'.jpg', '.jpeg'},
'image/png': {'.png'},
'application/pdf': {'.pdf'},
}
class UploadView(APIView):
authentication_classes = (
SessionAuthentication, OAuth2Authentication, DeviceTokenAuthentication, TeamTokenAuthentication
)
parser_classes = [FileUploadParser]
def post(self, request):
if 'file' not in request.data:
raise ValidationError('No file has been submitted.')
file_obj = request.data['file']
content_type = file_obj.content_type.split(";")[0] # ignore e.g. "; charset=…"
if content_type not in ALLOWED_TYPES:
raise ValidationError('Content type "{type}" is not allowed'.format(type=content_type))
if not any(file_obj.name.endswith(ext) for ext in ALLOWED_TYPES[content_type]):
raise ValidationError('File name "{name}" has an invalid extension for type "{type}"'.format(
name=file_obj.name,
type=content_type
))
cf = CachedFile.objects.create(
expires=now() + datetime.timedelta(days=1),
date=now(),
web_download=False,
filename=file_obj.name,
type=content_type,
session_key=f'api-upload-{str(type(request.user or request.auth))}-{(request.user or request.auth).pk}'
)
cf.file.save(file_obj.name, file_obj)
cf.save()
return Response({
'id': f'file:{cf.pk}'
}, status=201)

View File

@@ -344,6 +344,11 @@ REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
),
'TEST_REQUEST_RENDERER_CLASSES': [
'rest_framework.renderers.MultiPartRenderer',
'rest_framework.renderers.JSONRenderer',
'pretix.testutils.api.UploadRenderer',
],
'EXCEPTION_HANDLER': 'pretix.api.exception.custom_exception_handler',
'UNICODE_JSON': False
}

View File

@@ -0,0 +1,11 @@
from rest_framework.renderers import BaseRenderer
class UploadRenderer(BaseRenderer):
media_type = None
format = 'upload'
charset = 'utf-8'
def render(self, data, accepted_media_type=None, renderer_context=None):
self.media_type = data['media_type']
return data['file']

View File

@@ -0,0 +1,31 @@
import pytest
from django.core.files.base import ContentFile
@pytest.mark.django_db
def test_upload_file(token_client):
r = token_client.post(
'/api/v1/upload',
data={
'media_type': 'application/pdf',
'file': ContentFile('file.pdf', 'invalid pdf content')
},
format='upload',
HTTP_CONTENT_DISPOSITION='attachment; filename="file.pdf"',
)
assert r.status_code == 201
assert r.data['id'].startswith('file:')
@pytest.mark.django_db
def test_upload_file_extension_mismatch(token_client):
r = token_client.post(
'/api/v1/upload',
data={
'media_type': 'application/pdf',
'file': ContentFile('file.png', 'invalid pdf content')
},
format='upload',
HTTP_CONTENT_DISPOSITION='attachment; filename="file.png"',
)
assert r.status_code == 400