Skip to content

Commit 2d29d6c

Browse files
committed
[soc2009/http-wsgi-improvements] Initial HttpResponseSendFile support, changes pulled from 03/21/09 patch on refs django#2131.
This does not pass the included regression tests. However, since this feature will be entirely based on these changes, which have already gone through a great number of iterations, I thought it would be sensible to start here. All of the work here is ymasuda, mizatservercave, and mrts (apologies if I missed anyone). I hope to take their work down the final stretch. git-svn-id: http://code.djangoproject.com/svn/django/branches/soc2009/http-wsgi-improvements@11131 bcc190cf-cafb-0310-a4f2-bffc1f526a37
1 parent 4df7e8e commit 2d29d6c

File tree

12 files changed

+116
-12
lines changed

12 files changed

+116
-12
lines changed

django/conf/global_settings.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,10 @@
236236
# Example: "http://media.lawrence.com"
237237
MEDIA_URL = ''
238238

239+
# Header to use in HttpResponseSendFile to inform the handler to serve the
240+
# file with efficient handler-specific routines.
241+
HTTPRESPONSE_SENDFILE_HEADER = 'X-Sendfile'
242+
239243
# List of upload handler classes to be applied in order.
240244
FILE_UPLOAD_HANDLERS = (
241245
'django.core.files.uploadhandler.MemoryFileUploadHandler',

django/core/handlers/modpython.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -200,11 +200,14 @@ def __call__(self, req):
200200
for c in response.cookies.values():
201201
req.headers_out.add('Set-Cookie', c.output(header=''))
202202
req.status = response.status_code
203-
try:
204-
for chunk in response:
205-
req.write(chunk)
206-
finally:
207-
response.close()
203+
if isinstance(response, http.HttpResponseSendFile):
204+
req.sendfile(response.sendfile_filename)
205+
else:
206+
try:
207+
for chunk in response:
208+
req.write(chunk)
209+
finally:
210+
response.close()
208211

209212
return 0 # mod_python.apache.OK
210213

django/core/handlers/wsgi.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,5 +241,16 @@ def __call__(self, environ, start_response):
241241
for c in response.cookies.values():
242242
response_headers.append(('Set-Cookie', str(c.output(header=''))))
243243
start_response(status, response_headers)
244+
245+
if isinstance(response, http.HttpResponseSendFile):
246+
filelike = open(response.sendfile_filename, 'rb')
247+
if 'wsgi.file_wrapper' in environ:
248+
return environ['wsgi.file_wrapper'](filelike,
249+
response.block_size)
250+
else:
251+
# wraps close() as well
252+
from django.core.servers.basehttp import FileWrapper
253+
return FileWrapper(filelike, response.block_size)
254+
244255
return response
245256

django/core/servers/basehttp.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -314,10 +314,9 @@ def finish_response(self):
314314
to iterate over the data, and to call 'self.close()' once the response
315315
is finished.
316316
"""
317-
if not self.result_is_file() or not self.sendfile():
318-
for data in self.result:
319-
self.write(data)
320-
self.finish_content()
317+
for data in self.result:
318+
self.write(data)
319+
self.finish_content()
321320
self.close()
322321

323322
def get_scheme(self):

django/http/__init__.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,27 @@ def tell(self):
415415
raise Exception("This %s instance cannot tell its position" % self.__class__)
416416
return sum([len(chunk) for chunk in self._container])
417417

418+
class HttpResponseSendFile(HttpResponse):
419+
def __init__(self, path_to_file, content_type=None, block_size=8192):
420+
if not content_type:
421+
from mimetypes import guess_type
422+
content_type = guess_type(path_to_file)[0]
423+
if content_type is None:
424+
content_type = "application/octet-stream"
425+
super(HttpResponseSendFile, self).__init__(None,
426+
content_type=content_type)
427+
self.sendfile_filename = path_to_file
428+
self.block_size = block_size
429+
self['Content-Length'] = os.path.getsize(path_to_file)
430+
self['Content-Disposition'] = ('attachment; filename=%s' %
431+
os.path.basename(path_to_file))
432+
self[settings.HTTPRESPONSE_SENDFILE_HEADER] = path_to_file
433+
434+
def _get_content(self):
435+
return open(self.sendfile_filename)
436+
437+
content = property(_get_content)
438+
418439
class HttpResponseRedirect(HttpResponse):
419440
status_code = 302
420441

docs/ref/request-response.txt

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -560,9 +560,22 @@ Methods
560560
HttpResponse subclasses
561561
-----------------------
562562

563-
Django includes a number of ``HttpResponse`` subclasses that handle different
564-
types of HTTP responses. Like ``HttpResponse``, these subclasses live in
565-
:mod:`django.http`.
563+
Django includes a number of :class:`HttpResponse` subclasses that handle
564+
different types of HTTP responses. Like :class:`HttpResponse`, these subclasses
565+
live in :mod:`django.http`.
566+
567+
.. class:: HttpResponseSendFile
568+
569+
.. versionadded:: 1.1
570+
571+
A special response class for efficient file serving. It informs the HTTP
572+
protocol handler to use platform-specific file serving mechanism (if
573+
available). The constructor takes three arguments -- the file path and,
574+
optionally, the file's content type and block size hint for handlers that
575+
need it.
576+
577+
Note that response middleware will be bypassed if you use
578+
:class:`HttpResponseSendFile`.
566579

567580
.. class:: HttpResponseRedirect
568581

tests/regressiontests/sendfile/__init__.py

Whitespace-only changes.

tests/regressiontests/sendfile/models.py

Whitespace-only changes.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import urllib, os
2+
3+
from django.test import TestCase
4+
from django.conf import settings
5+
from django.core.files import temp as tempfile
6+
7+
FILE_SIZE = 2 ** 10
8+
CONTENT = 'a' * FILE_SIZE
9+
10+
class SendFileTests(TestCase):
11+
def test_sendfile(self):
12+
tdir = tempfile.gettempdir()
13+
14+
file1 = tempfile.NamedTemporaryFile(suffix=".pdf", dir=tdir)
15+
file1.write(CONTENT)
16+
file1.seek(0)
17+
18+
response = self.client.get('/sendfile/serve_file/%s/' %
19+
urllib.quote(file1.name))
20+
21+
file1.close()
22+
23+
self.assertEqual(response.status_code, 200)
24+
self.assertEqual(response[settings.HTTPRESPONSE_SENDFILE_HEADER],
25+
file1.name)
26+
self.assertEqual(response['Content-Disposition'],
27+
'attachment; filename=%s' % os.path.basename(file1.name))
28+
self.assertEqual(response['Content-Length'], str(FILE_SIZE))
29+
self.assertEqual(response['Content-Type'], 'application/pdf')
30+
31+
# *if* the degraded case is to be supported, add this instead:
32+
# self.assertEqual(response.content, CONTENT)
33+
get_content = lambda: response.content
34+
self.assertRaises(TypeError, get_content)
35+
36+
# TODO: test middleware bypass etc
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from django.conf.urls.defaults import patterns
2+
3+
import views
4+
5+
urlpatterns = patterns('',
6+
(r'^serve_file/(?P<filename>.*)/$', views.serve_file),
7+
)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import urllib
2+
3+
from django.http import HttpResponseSendFile
4+
5+
def serve_file(request, filename):
6+
filename = urllib.unquote(filename)
7+
return HttpResponseSendFile(filename)

tests/urls.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
# test urlconf for syndication tests
3434
(r'^syndication/', include('regressiontests.syndication.urls')),
3535

36+
# HttpResponseSendfile tests
37+
(r'^sendfile/', include('regressiontests.sendfile.urls')),
38+
3639
# conditional get views
3740
(r'condition/', include('regressiontests.conditional_processing.urls')),
3841
)

0 commit comments

Comments
 (0)