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,7 +78,11 @@ class DjangoStreamingServer(object): ...@@ -78,7 +78,11 @@ 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.
response = FileResponse(private_file.open()) if private_file.request.method == 'HEAD':
# Avoid reading the file at all
response = HttpResponse()
else:
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
response["Last-Modified"] = http_date(mtime) response["Last-Modified"] = http_date(mtime)
...@@ -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,26 +20,34 @@ class ViewTests(PrivateFileTestCase): ...@@ -19,26 +20,34 @@ 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')
# Initialize locally, no need for urls.py etc.. # Test HEAD calls too
# This behaves like a standard DetailView for method in ('GET', 'HEAD'):
view = PrivateStorageDetailView.as_view( request = RequestFactory().generic(method, '/cust1/file/')
model=CustomerDossier, request.user = superuser
slug_url_kwarg='customer',
slug_field='customer',
model_file_field='file'
)
response = view( # Initialize locally, no need for urls.py etc..
request, # This behaves like a standard DetailView
customer='cust1', view = PrivateStorageDetailView.as_view(
) model=CustomerDossier,
self.assertEqual(list(response.streaming_content), [b'test4']) slug_url_kwarg='customer',
self.assertEqual(response['Content-Type'], 'text/plain') slug_field='customer',
self.assertEqual(response['Content-Length'], '5') model_file_field='file'
self.assertIn('Last-Modified', response) )
response = view(
request,
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(response['Content-Type'], 'text/plain')
self.assertEqual(response['Content-Length'], '5')
self.assertIn('Last-Modified', response)
def test_private_file_view(self): def test_private_file_view(self):
""" """
...@@ -50,25 +59,32 @@ class ViewTests(PrivateFileTestCase): ...@@ -50,25 +59,32 @@ 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')
response = view( # Test HEAD calls too
request, for method in ('GET', 'HEAD'):
path='CustomerDossier/cust2/test5.txt' request = RequestFactory().generic(method, '/cust1/file/')
) request.user = superuser
self.assertEqual(list(response.streaming_content), [b'test5']) request.META['HTTP_USER_AGENT'] = 'Test'
self.assertEqual(response['Content-Type'], 'text/plain')
self.assertEqual(response['Content-Length'], '5') response = view(
self.assertEqual(response['Content-Disposition'], "attachment; filename*=UTF-8''test5.txt") request,
self.assertIn('Last-Modified', response) 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(response['Content-Type'], 'text/plain')
self.assertEqual(response['Content-Length'], '5')
self.assertEqual(response['Content-Disposition'], "attachment; filename*=UTF-8''test5.txt")
self.assertIn('Last-Modified', response)
def test_private_file_view_utf8(self): def test_private_file_view_utf8(self):
""" """
...@@ -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,16 +109,21 @@ class ViewTests(PrivateFileTestCase): ...@@ -93,16 +109,21 @@ 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.META['HTTP_USER_AGENT'] = user_agent request.user = superuser
request.META['HTTP_USER_AGENT'] = user_agent
response = view(
request, response = view(
path=u'CustomerDossier/cust2/Heizölrückstoßabdämpfung.txt' request,
) path=u'CustomerDossier/cust2/Heizölrückstoßabdämpfung.txt'
self.assertEqual(list(response.streaming_content), [b'test5']) )
self.assertEqual(response['Content-Type'], 'text/plain') if method == 'HEAD':
self.assertEqual(response['Content-Length'], '5') self.assertNotIsInstance(response, FileResponse)
self.assertEqual(response['Content-Disposition'], expect_header, user_agent) self.assertEqual(response.content, b'')
self.assertIn('Last-Modified', response) else:
self.assertEqual(list(response.streaming_content), [b'test5'])
self.assertEqual(response['Content-Type'], 'text/plain')
self.assertEqual(response['Content-Length'], '5')
self.assertEqual(response['Content-Disposition'], expect_header, user_agent)
self.assertIn('Last-Modified', response)
...@@ -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