From 4cecf5703e9e929f7f5c22074e350761bf98a5fb Mon Sep 17 00:00:00 2001 From: Gergely Date: Wed, 21 Aug 2024 11:11:31 +0200 Subject: [PATCH] Add support for direct serving --- binder/models.py | 8 +++++--- binder/views.py | 12 +++++++++--- tests/test_binder_file_field.py | 21 +++++++++++++++++++++ tests/testapp/models/zoo.py | 2 ++ tests/testapp/views/zoo.py | 3 ++- 5 files changed, 39 insertions(+), 7 deletions(-) diff --git a/binder/models.py b/binder/models.py index 717b4866..e1259643 100644 --- a/binder/models.py +++ b/binder/models.py @@ -825,12 +825,13 @@ class BinderFileField(FileField): attr_class = BinderFieldFile descriptor_class = BinderFileDescriptor - def __init__(self, allowed_extensions=None, *args, **kwargs): + def __init__(self, allowed_extensions=None, serve_directly=False, *args, **kwargs): # Since we also need to store a content type and a hash in the field # we up the default max_length from 100 to 200. Now we store also # the original file name, so lets make it 400 chars. kwargs.setdefault('max_length', 400) self.allowed_extensions = allowed_extensions + self.serve_directly = serve_directly return super().__init__(*args, **kwargs) def get_prep_value(self, value): @@ -860,6 +861,7 @@ def deconstruct(self): if self.allowed_extensions: kwargs['allowed_extensions'] = self.allowed_extensions + kwargs['serve_directly'] = self.serve_directly return name, path, args, kwargs @@ -903,11 +905,11 @@ class BinderImageField(BinderFileField): descriptor_class = BinderImageFileDescriptor description = _("Image") - def __init__(self, verbose_name=None, name=None, width_field=None, height_field=None, allowed_extensions=None, **kwargs): + def __init__(self, verbose_name=None, name=None, width_field=None, height_field=None, allowed_extensions=None, serve_directly=False, **kwargs): self.width_field, self.height_field = width_field, height_field if allowed_extensions is None: allowed_extensions = ['png', 'gif', 'jpg', 'jpeg'] - super().__init__(allowed_extensions, verbose_name, name, **kwargs) + super().__init__(allowed_extensions, serve_directly, verbose_name, name, **kwargs) def check(self, **kwargs): return [ diff --git a/binder/views.py b/binder/views.py index 4bec60f8..102c4cde 100644 --- a/binder/views.py +++ b/binder/views.py @@ -2870,15 +2870,21 @@ def dispatch_file_field(self, request, pk=None, file_field=None): file_field_name = file_field file_field = getattr(obj, file_field_name) + field = self.model._meta.get_field(file_field_name) if request.method == 'GET': if not file_field: raise BinderNotFound(file_field_name) - guess = mimetypes.guess_type(file_field.path) - guess = guess[0] if guess and guess[0] else 'application/octet-stream' + guess = mimetypes.guess_type(file_field.name) + content_type = (guess and guess[0]) or 'application/octet-stream' + serve_directly = isinstance(field, BinderFileField) and field.serve_directly try: - resp = StreamingHttpResponse(open(file_field.path, 'rb'), content_type=guess) + if serve_directly: + resp = HttpResponse(content_type=content_type) + resp['X-Accel-Redirect'] = '/internal/media/' + file_field.name + else: + resp = StreamingHttpResponse(open(file_field.path, 'rb'), content_type=content_type) except FileNotFoundError: logger.error('Expected file {} not found'.format(file_field.path)) raise BinderNotFound(file_field_name) diff --git a/tests/test_binder_file_field.py b/tests/test_binder_file_field.py index 9fc901eb..193d759d 100644 --- a/tests/test_binder_file_field.py +++ b/tests/test_binder_file_field.py @@ -146,6 +146,7 @@ def test_get(self): data['data']['binder_picture'], '/zoo/{}/binder_picture/?h={}&content_type=image/jpeg&filename={}'.format(zoo.pk, JPG_HASH, filename), ) + self.assertNotIn('X-Accel-Redirect', response.headers) def test_get_unknown_extension(self): filename = 'pic.unknown' @@ -166,6 +167,26 @@ def test_get_unknown_extension(self): '/zoo/{}/binder_picture/?h={}&content_type=&filename={}'.format(zoo.pk, UNKNOWN_TYPE_HASH, filename), ) + def test_get_direct(self): + filename = 'pic.jpg' + zoo = Zoo(name='Apenheul') + zoo.binder_picture_direct = ContentFile(JPG_CONTENT, name=filename) + zoo.save() + + response = self.client.get('/zoo/{}/'.format(zoo.pk)) + self.assertEqual(response.status_code, 200) + data = jsonloads(response.content) + + # Remove once Django 3 lands with: https://docs.djangoproject.com/en/3.1/howto/custom-file-storage/#django.core.files.storage.get_alternative_name + zoo.refresh_from_db() + filename = basename(zoo.binder_picture_direct.name) # Without folders foo/bar/ + + self.assertEqual( + data['data']['binder_picture_direct'], + '/zoo/{}/binder_picture_direct/?h={}&content_type=image/jpeg&filename={}'.format(zoo.pk, JPG_HASH, filename), + ) + self.assertIn('X-Accel-Redirect', response.headers) + def test_setting_blank(self): zoo = Zoo(name='Apenheul') zoo.binder_picture = '' diff --git a/tests/testapp/models/zoo.py b/tests/testapp/models/zoo.py index 0637f51f..0aff9c2a 100644 --- a/tests/testapp/models/zoo.py +++ b/tests/testapp/models/zoo.py @@ -43,6 +43,8 @@ class Binder: binder_picture_custom_extensions = BinderImageField(allowed_extensions=['png'], blank=True, null=True) + binder_picture_direct = BinderImageField(serve_directly=True, blank=True, null=True) + def __str__(self): return 'zoo %d: %s' % (self.pk, self.name) diff --git a/tests/testapp/views/zoo.py b/tests/testapp/views/zoo.py index 0f7916ef..1f79917e 100644 --- a/tests/testapp/views/zoo.py +++ b/tests/testapp/views/zoo.py @@ -11,12 +11,13 @@ class ZooView(PermissionView): m2m_fields = ['contacts', 'zoo_employees', 'most_popular_animals'] model = Zoo file_fields = ['floor_plan', 'django_picture', 'binder_picture', 'django_picture_not_null', - 'binder_picture_not_null', 'binder_picture_custom_extensions'] + 'binder_picture_not_null', 'binder_picture_custom_extensions', 'binder_picture_direct'] shown_properties = ['animal_count'] image_resize_threshold = { 'floor_plan': 500, 'binder_picture': 500, 'binder_picture_custom_extensions': 500, + 'binder_picture_direct': 500, } image_format_override = { 'floor_plan': 'jpeg',