diff --git a/binder/models.py b/binder/models.py index 3f50e3b7..c45e41ae 100644 --- a/binder/models.py +++ b/binder/models.py @@ -790,12 +790,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): @@ -825,6 +826,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 @@ -868,11 +870,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 511dee52..796015ff 100644 --- a/binder/views.py +++ b/binder/views.py @@ -14,6 +14,7 @@ import django from django.views.generic import View +from django.conf import settings from django.core.exceptions import ObjectDoesNotExist, FieldError, ValidationError, FieldDoesNotExist from django.core.files.base import File, ContentFile from django.http import HttpResponse, StreamingHttpResponse, HttpResponseForbidden @@ -486,7 +487,7 @@ def dispatch(self, request, *args, **kwargs): # Check if the TRANSACTION_DATABASES is set in the settings.py, and if so, use that instead try: - transaction_dbs = django.conf.settings.TRANSACTION_DATABASES + transaction_dbs = settings.TRANSACTION_DATABASES except AttributeError: pass @@ -1537,7 +1538,7 @@ def get(self, request, pk=None, withs=None, include_annotations=None): meta['comment'] = self.comment debug = {'request_id': request.request_id} - if django.conf.settings.DEBUG and 'debug' in request.GET: + if settings.DEBUG and 'debug' in request.GET: debug['queries'] = ['{}s: {}'.format(q['time'], q['sql'].replace('"', '')) for q in django.db.connection.queries] debug['query_count'] = len(django.db.connection.queries) @@ -2687,15 +2688,24 @@ 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.name) - guess = guess[0] if guess and guess[0] else 'application/octet-stream' + content_type = (guess and guess[0]) or 'application/octet-stream' + serve_directly = isinstance(field, BinderFileField) and field.serve_directly try: - resp = StreamingHttpResponse(file_field.open(), content_type=guess) + if serve_directly: + resp = HttpResponse(content_type=content_type) + if file_field.url.startswith('/'): + resp[settings.INTERNAL_MEDIA_HEADER] = os.path.join(settings.INTERNAL_MEDIA_LOCATION, file_field.name) + else: + resp[settings.INTERNAL_MEDIA_HEADER] = file_field.url + else: + resp = StreamingHttpResponse(file_field.open(), content_type=content_type) except FileNotFoundError: logger.error('Expected file {} not found'.format(file_field.name)) raise BinderNotFound(file_field_name) @@ -2760,7 +2770,7 @@ def view_history(self, request, pk=None, **kwargs): debug = kwargs['history'] == 'debug' - if debug and not django.conf.settings.ENABLE_DEBUG_ENDPOINTS: + if debug and not settings.ENABLE_DEBUG_ENDPOINTS: logger.warning('Debug endpoints disabled.') return HttpResponseForbidden('Debug endpoints disabled.') @@ -2790,7 +2800,7 @@ def debug_changesets_24h(request): logger.warning('Not authenticated.') return HttpResponseForbidden('Not authenticated.') - if not django.conf.settings.ENABLE_DEBUG_ENDPOINTS: + if not settings.ENABLE_DEBUG_ENDPOINTS: logger.warning('Debug endpoints disabled.') return HttpResponseForbidden('Debug endpoints disabled.') diff --git a/tests/__init__.py b/tests/__init__.py index 4dcaf8bd..0d8e9148 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -108,7 +108,9 @@ }, 'GROUP_CONTAINS': { 'admin': [] - } + }, + 'INTERNAL_MEDIA_HEADER': 'X-Accel-Redirect', + 'INTERNAL_MEDIA_LOCATION': '/internal/media/', }) setup() @@ -131,4 +133,3 @@ content_type = ContentType.objects.get_or_create(app_label='testapp', model='country')[0] Permission.objects.get_or_create(content_type=content_type, codename='view_country') call_command('define_groups') - diff --git a/tests/test_binder_file_field.py b/tests/test_binder_file_field.py index 9fc901eb..f6e8a0d7 100644 --- a/tests/test_binder_file_field.py +++ b/tests/test_binder_file_field.py @@ -142,10 +142,10 @@ def test_get(self): zoo.refresh_from_db() filename = basename(zoo.binder_picture.name) # Without folders foo/bar/ - self.assertEqual( - data['data']['binder_picture'], - '/zoo/{}/binder_picture/?h={}&content_type=image/jpeg&filename={}'.format(zoo.pk, JPG_HASH, filename), - ) + path = '/zoo/{}/binder_picture/?h={}&content_type=image/jpeg&filename={}'.format(zoo.pk, JPG_HASH, filename) + self.assertEqual(data['data']['binder_picture'], path) + response = self.client.get(path) + self.assertNotIn('X-Accel-Redirect', response.headers) def test_get_unknown_extension(self): filename = 'pic.unknown' @@ -166,6 +166,25 @@ 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/ + + path = '/zoo/{}/binder_picture_direct/?h={}&content_type=image/jpeg&filename={}'.format(zoo.pk, JPG_HASH, filename) + self.assertEqual(data['data']['binder_picture_direct'], path) + response = self.client.get(path) + 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 a6b1737c..859f7099 100644 --- a/tests/testapp/models/zoo.py +++ b/tests/testapp/models/zoo.py @@ -35,6 +35,8 @@ class Zoo(BinderModel): 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 813b5bde..a22f2d6c 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',