mirror of
https://github.com/pretix/pretix.git
synced 2026-05-04 15:04:03 +00:00
API: File upload infrastructure
This commit is contained in:
@@ -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"),
|
||||
]
|
||||
|
||||
53
src/pretix/api/views/upload.py
Normal file
53
src/pretix/api/views/upload.py
Normal 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)
|
||||
@@ -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
|
||||
}
|
||||
|
||||
11
src/pretix/testutils/api.py
Normal file
11
src/pretix/testutils/api.py
Normal 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']
|
||||
31
src/tests/api/test_upload.py
Normal file
31
src/tests/api/test_upload.py
Normal 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
|
||||
Reference in New Issue
Block a user