Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

[soc2009/http-wsgi-improvements] Initial HttpResponseSendFile support…

…, changes pulled from 03/21/09 patch on refs #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
  • Loading branch information...
commit 2d29d6c4f83449fb4ff4d728720a15762c1653b5 1 parent 4df7e8e
Christopher Cahoon authored July 01, 2009
4  django/conf/global_settings.py
@@ -236,6 +236,10 @@
236 236
 # Example: "http://media.lawrence.com"
237 237
 MEDIA_URL = ''
238 238
 
  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
+
239 243
 # List of upload handler classes to be applied in order.
240 244
 FILE_UPLOAD_HANDLERS = (
241 245
     'django.core.files.uploadhandler.MemoryFileUploadHandler',
13  django/core/handlers/modpython.py
@@ -200,11 +200,14 @@ def __call__(self, req):
200 200
         for c in response.cookies.values():
201 201
             req.headers_out.add('Set-Cookie', c.output(header=''))
202 202
         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()
208 211
 
209 212
         return 0 # mod_python.apache.OK
210 213
 
11  django/core/handlers/wsgi.py
@@ -241,5 +241,16 @@ def __call__(self, environ, start_response):
241 241
         for c in response.cookies.values():
242 242
             response_headers.append(('Set-Cookie', str(c.output(header=''))))
243 243
         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
+                
244 255
         return response
245 256
 
7  django/core/servers/basehttp.py
@@ -314,10 +314,9 @@ def finish_response(self):
314 314
         to iterate over the data, and to call 'self.close()' once the response
315 315
         is finished.
316 316
         """
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()
321 320
         self.close()
322 321
 
323 322
     def get_scheme(self):
21  django/http/__init__.py
@@ -415,6 +415,27 @@ def tell(self):
415 415
             raise Exception("This %s instance cannot tell its position" % self.__class__)
416 416
         return sum([len(chunk) for chunk in self._container])
417 417
 
  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
+
418 439
 class HttpResponseRedirect(HttpResponse):
419 440
     status_code = 302
420 441
 
19  docs/ref/request-response.txt
@@ -560,9 +560,22 @@ Methods
560 560
 HttpResponse subclasses
561 561
 -----------------------
562 562
 
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`.
566 579
 
567 580
 .. class:: HttpResponseRedirect
568 581
 
0  tests/regressiontests/sendfile/__init__.py
No changes.
0  tests/regressiontests/sendfile/models.py
No changes.
36  tests/regressiontests/sendfile/tests.py
... ...
@@ -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
7  tests/regressiontests/sendfile/urls.py
... ...
@@ -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
+)
7  tests/regressiontests/sendfile/views.py
... ...
@@ -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)
3  tests/urls.py
@@ -33,6 +33,9 @@
33 33
     # test urlconf for syndication tests
34 34
     (r'^syndication/', include('regressiontests.syndication.urls')),
35 35
 
  36
+    # HttpResponseSendfile tests 
  37
+ 	(r'^sendfile/', include('regressiontests.sendfile.urls')), 
  38
+
36 39
     # conditional get views
37 40
     (r'condition/', include('regressiontests.conditional_processing.urls')),
38 41
 )

0 notes on commit 2d29d6c

Please sign in to comment.
Something went wrong with that request. Please try again.