Unverified Commit d0f44821 authored by Diederik van der Boor's avatar Diederik van der Boor
Browse files

Added efficient HEAD request handling

This is not implemented using a `def head()` in the view, as the HEAD request
should return all headers that the GET request would send. Hence,
updated the server logic to provide a proper HEAD request.
parent ca0d9a6c
...@@ -78,6 +78,10 @@ class DjangoStreamingServer(object): ...@@ -78,6 +78,10 @@ class DjangoStreamingServer(object):
# As of Django 1.8, FileResponse triggers 'wsgi.file_wrapper' in Django's WSGIHandler. # As of Django 1.8, FileResponse triggers 'wsgi.file_wrapper' in Django's WSGIHandler.
# This uses efficient file streaming, such as sendfile() in uWSGI. # This uses efficient file streaming, such as sendfile() in uWSGI.
# When the WSGI container doesn't provide 'wsgi.file_wrapper', it submits the file in 4KB chunks. # When the WSGI container doesn't provide 'wsgi.file_wrapper', it submits the file in 4KB chunks.
if private_file.request.method == 'HEAD':
# Avoid reading the file at all
response = HttpResponse()
else:
response = FileResponse(private_file.open()) response = FileResponse(private_file.open())
response['Content-Type'] = private_file.content_type response['Content-Type'] = private_file.content_type
response['Content-Length'] = size response['Content-Length'] = size
...@@ -113,7 +117,15 @@ class DjangoServer(DjangoStreamingServer): ...@@ -113,7 +117,15 @@ class DjangoServer(DjangoStreamingServer):
return DjangoStreamingServer.serve(private_file) return DjangoStreamingServer.serve(private_file)
else: else:
# Using Django's serve gives If-Modified-Since support out of the box. # Using Django's serve gives If-Modified-Since support out of the box.
return serve(private_file.request, full_path, document_root='/', show_indexes=False) response = serve(private_file.request, full_path, document_root='/', show_indexes=False)
if private_file.request.method == 'HEAD':
# Avoid reading the file at all, copy FileResponse headers
head_response = HttpResponse(status=response.status_code)
for header, value in response.items():
head_response[header] = value
return head_response
else:
return response
class ApacheXSendfileServer(object): class ApacheXSendfileServer(object):
......
# encoding: utf-8 # encoding: utf-8
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.uploadedfile import SimpleUploadedFile
from django.http import FileResponse
from django.test import RequestFactory from django.test import RequestFactory
from private_storage.tests.models import CustomerDossier from private_storage.tests.models import CustomerDossier
...@@ -19,8 +20,12 @@ class ViewTests(PrivateFileTestCase): ...@@ -19,8 +20,12 @@ class ViewTests(PrivateFileTestCase):
file=SimpleUploadedFile('test4.txt', b'test4') file=SimpleUploadedFile('test4.txt', b'test4')
) )
request = RequestFactory().get('/cust1/file/') superuser = User.objects.create_superuser('admin', 'admin@example.com', 'admin')
request.user = User.objects.create_superuser('admin', 'admin@example.com', 'admin')
# Test HEAD calls too
for method in ('GET', 'HEAD'):
request = RequestFactory().generic(method, '/cust1/file/')
request.user = superuser
# Initialize locally, no need for urls.py etc.. # Initialize locally, no need for urls.py etc..
# This behaves like a standard DetailView # This behaves like a standard DetailView
...@@ -35,6 +40,10 @@ class ViewTests(PrivateFileTestCase): ...@@ -35,6 +40,10 @@ class ViewTests(PrivateFileTestCase):
request, request,
customer='cust1', customer='cust1',
) )
if method == 'HEAD':
self.assertNotIsInstance(response, FileResponse)
self.assertEqual(response.content, b'')
else:
self.assertEqual(list(response.streaming_content), [b'test4']) self.assertEqual(list(response.streaming_content), [b'test4'])
self.assertEqual(response['Content-Type'], 'text/plain') self.assertEqual(response['Content-Type'], 'text/plain')
self.assertEqual(response['Content-Length'], '5') self.assertEqual(response['Content-Length'], '5')
...@@ -50,20 +59,27 @@ class ViewTests(PrivateFileTestCase): ...@@ -50,20 +59,27 @@ class ViewTests(PrivateFileTestCase):
) )
self.assertExists('CustomerDossier', 'cust2', 'test5.txt') self.assertExists('CustomerDossier', 'cust2', 'test5.txt')
request = RequestFactory().get('/cust1/file/')
request.user = User.objects.create_superuser('admin', 'admin@example.com', 'admin')
request.META['HTTP_USER_AGENT'] = 'Test'
# Initialize locally, no need for urls.py etc.. # Initialize locally, no need for urls.py etc..
# This behaves like a standard DetailView # This behaves like a standard DetailView
view = PrivateStorageView.as_view( view = PrivateStorageView.as_view(
content_disposition='attachment', content_disposition='attachment',
) )
superuser = User.objects.create_superuser('admin', 'admin@example.com', 'admin')
# Test HEAD calls too
for method in ('GET', 'HEAD'):
request = RequestFactory().generic(method, '/cust1/file/')
request.user = superuser
request.META['HTTP_USER_AGENT'] = 'Test'
response = view( response = view(
request, request,
path='CustomerDossier/cust2/test5.txt' path='CustomerDossier/cust2/test5.txt'
) )
if method == 'HEAD':
self.assertNotIsInstance(response, FileResponse)
self.assertEqual(response.content, b'')
else:
self.assertEqual(list(response.streaming_content), [b'test5']) self.assertEqual(list(response.streaming_content), [b'test5'])
self.assertEqual(response['Content-Type'], 'text/plain') self.assertEqual(response['Content-Type'], 'text/plain')
self.assertEqual(response['Content-Length'], '5') self.assertEqual(response['Content-Length'], '5')
...@@ -85,7 +101,7 @@ class ViewTests(PrivateFileTestCase): ...@@ -85,7 +101,7 @@ class ViewTests(PrivateFileTestCase):
view = PrivateStorageView.as_view( view = PrivateStorageView.as_view(
content_disposition='attachment', content_disposition='attachment',
) )
admin = User.objects.create_superuser('admin', 'admin@example.com', 'admin') superuser = User.objects.create_superuser('admin', 'admin@example.com', 'admin')
for user_agent, expect_header in [ for user_agent, expect_header in [
('Firefox', "attachment; filename*=UTF-8''Heiz%C3%B6lr%C3%BCcksto%C3%9Fabd%C3%A4mpfung.txt"), ('Firefox', "attachment; filename*=UTF-8''Heiz%C3%B6lr%C3%BCcksto%C3%9Fabd%C3%A4mpfung.txt"),
...@@ -93,14 +109,19 @@ class ViewTests(PrivateFileTestCase): ...@@ -93,14 +109,19 @@ class ViewTests(PrivateFileTestCase):
('MSIE', 'attachment; filename=Heiz%C3%B6lr%C3%BCcksto%C3%9Fabd%C3%A4mpfung.txt'), ('MSIE', 'attachment; filename=Heiz%C3%B6lr%C3%BCcksto%C3%9Fabd%C3%A4mpfung.txt'),
]: ]:
request = RequestFactory().get('/cust1/file/') for method in ('GET', 'HEAD'):
request.user = admin request = RequestFactory().generic(method, '/cust1/file/')
request.user = superuser
request.META['HTTP_USER_AGENT'] = user_agent request.META['HTTP_USER_AGENT'] = user_agent
response = view( response = view(
request, request,
path=u'CustomerDossier/cust2/Heizölrückstoßabdämpfung.txt' path=u'CustomerDossier/cust2/Heizölrückstoßabdämpfung.txt'
) )
if method == 'HEAD':
self.assertNotIsInstance(response, FileResponse)
self.assertEqual(response.content, b'')
else:
self.assertEqual(list(response.streaming_content), [b'test5']) self.assertEqual(list(response.streaming_content), [b'test5'])
self.assertEqual(response['Content-Type'], 'text/plain') self.assertEqual(response['Content-Type'], 'text/plain')
self.assertEqual(response['Content-Length'], '5') self.assertEqual(response['Content-Length'], '5')
......
...@@ -3,7 +3,7 @@ Views to send private files. ...@@ -3,7 +3,7 @@ Views to send private files.
""" """
import os import os
from django.http import Http404, HttpResponseForbidden from django.http import Http404, HttpResponse, HttpResponseForbidden
from django.utils.module_loading import import_string from django.utils.module_loading import import_string
from django.views.generic import View from django.views.generic import View
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment