1. Luke Plant
  2. django

Commits

cca...@bcc190cf-cafb-0310-a4f2-bffc1f526a37  committed 9ed8f6a

[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.

  • Participants
  • Parent commits 7534a03
  • Branches soc2009/http-wsgi-improvements

Comments (0)

Files changed (12)

File django/conf/global_settings.py

View file
 # Example: "http://media.lawrence.com"
 MEDIA_URL = ''
 
+# Header to use in HttpResponseSendFile to inform the handler to serve the 
+# file with efficient handler-specific routines. 
+HTTPRESPONSE_SENDFILE_HEADER = 'X-Sendfile' 
+
 # List of upload handler classes to be applied in order.
 FILE_UPLOAD_HANDLERS = (
     'django.core.files.uploadhandler.MemoryFileUploadHandler',

File django/core/handlers/modpython.py

View file
         for c in response.cookies.values():
             req.headers_out.add('Set-Cookie', c.output(header=''))
         req.status = response.status_code
-        try:
-            for chunk in response:
-                req.write(chunk)
-        finally:
-            response.close()
+        if isinstance(response, http.HttpResponseSendFile): 
+            req.sendfile(response.sendfile_filename) 
+        else:
+            try:
+                for chunk in response:
+                    req.write(chunk)
+            finally:
+                response.close()
 
         return 0 # mod_python.apache.OK
 

File django/core/handlers/wsgi.py

View file
         for c in response.cookies.values():
             response_headers.append(('Set-Cookie', str(c.output(header=''))))
         start_response(status, response_headers)
+        
+        if isinstance(response, http.HttpResponseSendFile): 
+            filelike = open(response.sendfile_filename, 'rb') 
+            if 'wsgi.file_wrapper' in environ: 
+                return environ['wsgi.file_wrapper'](filelike, 
+                        response.block_size) 
+            else: 
+                # wraps close() as well 
+                from django.core.servers.basehttp import FileWrapper 
+                return FileWrapper(filelike, response.block_size) 
+                
         return response
 

File django/core/servers/basehttp.py

View file
         to iterate over the data, and to call 'self.close()' once the response
         is finished.
         """
-        if not self.result_is_file() or not self.sendfile():
-            for data in self.result:
-                self.write(data)
-            self.finish_content()
+        for data in self.result:
+            self.write(data)
+        self.finish_content()
         self.close()
 
     def get_scheme(self):

File django/http/__init__.py

View file
             raise Exception("This %s instance cannot tell its position" % self.__class__)
         return sum([len(chunk) for chunk in self._container])
 
+class HttpResponseSendFile(HttpResponse): 
+    def __init__(self, path_to_file, content_type=None, block_size=8192): 
+ 	    if not content_type: 
+ 	        from mimetypes import guess_type 
+ 	        content_type = guess_type(path_to_file)[0] 
+ 	        if content_type is None: 
+ 	            content_type = "application/octet-stream" 
+ 	    super(HttpResponseSendFile, self).__init__(None, 
+ 	            content_type=content_type) 
+ 	    self.sendfile_filename = path_to_file 
+ 	    self.block_size = block_size 
+ 	    self['Content-Length'] = os.path.getsize(path_to_file) 
+ 	    self['Content-Disposition'] = ('attachment; filename=%s' % 
+ 	            os.path.basename(path_to_file)) 
+ 	    self[settings.HTTPRESPONSE_SENDFILE_HEADER] = path_to_file 
+
+    def _get_content(self):
+        return open(self.sendfile_filename)
+
+    content = property(_get_content)
+
 class HttpResponseRedirect(HttpResponse):
     status_code = 302
 

File docs/ref/request-response.txt

View file
 HttpResponse subclasses
 -----------------------
 
-Django includes a number of ``HttpResponse`` subclasses that handle different
-types of HTTP responses. Like ``HttpResponse``, these subclasses live in
-:mod:`django.http`.
+Django includes a number of :class:`HttpResponse` subclasses that handle
+different types of HTTP responses. Like :class:`HttpResponse`, these subclasses
+live in :mod:`django.http`.
+
+.. class:: HttpResponseSendFile
+
+    .. versionadded:: 1.1
+
+    A special response class for efficient file serving. It informs the HTTP
+    protocol handler to use platform-specific file serving mechanism (if
+    available). The constructor takes three arguments -- the file path and,
+    optionally, the file's content type and block size hint for handlers that
+    need it.
+
+    Note that response middleware will be bypassed if you use
+    :class:`HttpResponseSendFile`.
 
 .. class:: HttpResponseRedirect
 

File tests/regressiontests/sendfile/__init__.py

Empty file added.

File tests/regressiontests/sendfile/models.py

Empty file added.

File tests/regressiontests/sendfile/tests.py

View file
+import urllib, os
+
+from django.test import TestCase
+from django.conf import settings
+from django.core.files import temp as tempfile
+
+FILE_SIZE = 2 ** 10
+CONTENT = 'a' * FILE_SIZE
+
+class SendFileTests(TestCase):
+    def test_sendfile(self):
+        tdir = tempfile.gettempdir()
+
+        file1 = tempfile.NamedTemporaryFile(suffix=".pdf", dir=tdir)
+        file1.write(CONTENT)
+        file1.seek(0)
+
+        response = self.client.get('/sendfile/serve_file/%s/' %
+                urllib.quote(file1.name))
+
+        file1.close()
+
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response[settings.HTTPRESPONSE_SENDFILE_HEADER],
+                file1.name)
+        self.assertEqual(response['Content-Disposition'],
+                'attachment; filename=%s' % os.path.basename(file1.name))
+        self.assertEqual(response['Content-Length'], str(FILE_SIZE))
+        self.assertEqual(response['Content-Type'], 'application/pdf')
+
+        # *if* the degraded case is to be supported, add this instead:
+        # self.assertEqual(response.content, CONTENT)
+        get_content = lambda: response.content
+        self.assertRaises(TypeError, get_content)
+
+        # TODO: test middleware bypass etc

File tests/regressiontests/sendfile/urls.py

View file
+from django.conf.urls.defaults import patterns
+
+import views
+
+urlpatterns = patterns('',
+    (r'^serve_file/(?P<filename>.*)/$', views.serve_file),
+)

File tests/regressiontests/sendfile/views.py

View file
+import urllib
+
+from django.http import HttpResponseSendFile
+
+def serve_file(request, filename):
+    filename = urllib.unquote(filename)
+    return HttpResponseSendFile(filename)

File tests/urls.py

View file
     # test urlconf for syndication tests
     (r'^syndication/', include('regressiontests.syndication.urls')),
 
+    # HttpResponseSendfile tests 
+ 	(r'^sendfile/', include('regressiontests.sendfile.urls')), 
+
     # conditional get views
     (r'condition/', include('regressiontests.conditional_processing.urls')),
 )