diff --git a/demo/demoproject/download/tests.py b/demo/demoproject/download/tests.py index b006131..6e426f5 100644 --- a/demo/demoproject/download/tests.py +++ b/demo/demoproject/download/tests.py @@ -10,6 +10,8 @@ from django.test import TestCase from django.test.utils import override_settings +from django_downloadview.nginx import XAccelRedirectResponse + from demoproject.download.models import Document @@ -98,3 +100,24 @@ def test_download_hello_world(self): 'attachment; filename=hello-world.txt') self.assertEqual(open(self.files['hello-world.txt']).read(), response.content) + + +class XAccelRedirectDecoratorTestCase(DownloadTestCase): + @temporary_media_root() + def test_response(self): + document = Document.objects.create( + slug='hello-world', + file=File(open(self.files['hello-world.txt'])), + ) + download_url = reverse('download_document_nginx', + kwargs={'slug': 'hello-world'}) + response = self.client.get(download_url) + self.assertEquals(response.status_code, 200) + self.assertTrue(isinstance(response, XAccelRedirectResponse)) + self.assertEquals(response['Content-Type'], + 'text/plain; charset=utf-8') + self.assertFalse('ContentEncoding' in response) + self.assertEquals(response['Content-Disposition'], + 'attachment; filename=hello-world.txt') + self.assertEquals(response['X-Accel-Redirect'], + '/download-optimized/document/hello-world.txt') diff --git a/demo/demoproject/download/urls.py b/demo/demoproject/download/urls.py index 477aa40..32da47d 100644 --- a/demo/demoproject/download/urls.py +++ b/demo/demoproject/download/urls.py @@ -7,4 +7,6 @@ name='download_hello_world'), url(r'^document/(?P[a-zA-Z0-9_-]+)/$', 'download_document', name='download_document'), + url(r'^document-nginx/(?P[a-zA-Z0-9_-]+)/$', + 'download_document_nginx', name='download_document_nginx'), ) diff --git a/demo/demoproject/download/views.py b/demo/demoproject/download/views.py index 6ba9854..6c46508 100644 --- a/demo/demoproject/download/views.py +++ b/demo/demoproject/download/views.py @@ -1,6 +1,7 @@ from os.path import abspath, dirname, join from django_downloadview import DownloadView, ObjectDownloadView +from django_downloadview.nginx import x_accel_redirect from demoproject.download.models import Document @@ -14,3 +15,7 @@ storage=None) download_document = ObjectDownloadView.as_view(model=Document) + +download_document_nginx = x_accel_redirect(download_document, + media_root='/var/www/files', + media_url='/download-optimized') diff --git a/django_downloadview/nginx.py b/django_downloadview/nginx.py index aaf2067..2c8035b 100644 --- a/django_downloadview/nginx.py +++ b/django_downloadview/nginx.py @@ -6,10 +6,12 @@ from datetime import datetime, timedelta from django.conf import settings +from django.core.exceptions import ImproperlyConfigured from django.http import HttpResponse -from django_downloadview.middlewares import BaseDownloadMiddleware from django_downloadview.decorators import DownloadDecorator +from django_downloadview.middlewares import BaseDownloadMiddleware +from django_downloadview.utils import content_type_to_charset #: Default value for X-Accel-Buffering header. @@ -24,19 +26,15 @@ setattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_LIMIT', DEFAULT_LIMIT_RATE) -def content_type_to_charset(content_type): - return 'utf-8' - - class XAccelRedirectResponse(HttpResponse): """Http response that delegate serving file to Nginx.""" - def __init__(self, url, content_type, basename=None, expires=None, + def __init__(self, redirect_url, content_type, basename=None, expires=None, with_buffering=None, limit_rate=None): """Return a HttpResponse with headers for Nginx X-Accel-Redirect.""" super(XAccelRedirectResponse, self).__init__(content_type=content_type) - basename = basename or url.split('/')[-1] + basename = basename or redirect_url.split('/')[-1] self['Content-Disposition'] = 'attachment; filename=%s' % basename - self['X-Accel-Redirect'] = url + self['X-Accel-Redirect'] = redirect_url self['X-Accel-Charset'] = content_type_to_charset(content_type) if with_buffering is not None: self['X-Accel-Buffering'] = with_buffering and 'yes' or 'no' @@ -52,18 +50,25 @@ def __init__(self, url, content_type, basename=None, expires=None, class BaseXAccelRedirectMiddleware(BaseDownloadMiddleware): """Looks like a middleware, but configurable.""" - def __init__(self, expires=None, with_buffering=None, limit_rate=None): + def __init__(self, media_root, media_url, expires=None, + with_buffering=None, limit_rate=None): """Constructor.""" + self.media_root = media_root + self.media_url = media_url self.expires = expires self.with_buffering = with_buffering self.limit_rate = limit_rate - def file_to_url(response): - return response.filename + def get_redirect_url(self, response): + """Return redirect URL for file wrapped into response.""" + absolute_filename = response.filename + relative_filename = absolute_filename[len(self.media_root):] + return '/'.join((self.media_url.rstrip('/'), + relative_filename.strip('/'))) def process_download_response(self, request, response): """Replace DownloadResponse instances by NginxDownloadResponse ones.""" - url = self.file_to_url(response) + redirect_url = self.get_redirect_url(response) if self.expires: expires = self.expires else: @@ -71,8 +76,8 @@ def process_download_response(self, request, response): expires = response.expires except AttributeError: expires = None - return XAccelRedirectResponse(url=url, - content_type=response.content_type, + return XAccelRedirectResponse(redirect_url=redirect_url, + content_type=response['Content-Type'], basename=response.basename, expires=expires, with_buffering=self.with_buffering, @@ -87,8 +92,22 @@ class XAccelRedirectMiddleware(): """ def __init__(self): """Use Django settings as configuration.""" + try: + media_root = settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT + except AttributeError: + raise ImproperlyConfigured( + 'settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT is required by ' + '%s middleware' % self.__class__.name) + try: + media_url = settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL + except AttributeError: + raise ImproperlyConfigured( + 'settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL is required by ' + '%s middleware' % self.__class__.name) super(XAccelRedirectMiddleware, self).__init__( - expires=settings.NGINX_DOWNLOAD_MIDDLEWARE_EXPIRESS, + media_root, + media_url, + expires=settings.NGINX_DOWNLOAD_MIDDLEWARE_EXPIRES, with_buffering=settings.NGINX_DOWNLOAD_MIDDLEWARE_WITH_BUFFERING, limit_rate=settings.NGINX_DOWNLOAD_MIDDLEWARE_LIMIT_RATE) diff --git a/django_downloadview/utils.py b/django_downloadview/utils.py new file mode 100644 index 0000000..daddb05 --- /dev/null +++ b/django_downloadview/utils.py @@ -0,0 +1,18 @@ +"""Utility functions.""" +import re + + +charset_pattern = re.compile(r'charset=(?P.+)$', re.I | re.U) + + +def content_type_to_charset(content_type): + """Return charset part of content-type header. + + >>> from django_downloadview.utils import content_type_to_charset + >>> content_type_to_charset('text/html; charset=utf-8') + 'utf-8' + + """ + match = re.search(charset_pattern, content_type) + if match: + return match.group('charset') diff --git a/docs/api/django_downloadview.txt b/docs/api/django_downloadview.txt index e54f5cc..cc5f612 100644 --- a/docs/api/django_downloadview.txt +++ b/docs/api/django_downloadview.txt @@ -17,6 +17,14 @@ django_downloadview Package :undoc-members: :show-inheritance: +:mod:`file` Module +------------------ + +.. automodule:: django_downloadview.file + :members: + :undoc-members: + :show-inheritance: + :mod:`middlewares` Module ------------------------- @@ -41,6 +49,14 @@ django_downloadview Package :undoc-members: :show-inheritance: +:mod:`utils` Module +------------------- + +.. automodule:: django_downloadview.utils + :members: + :undoc-members: + :show-inheritance: + :mod:`views` Module ------------------- diff --git a/docs/nginx.txt b/docs/nginx.txt index 69d39ba..4b11147 100644 --- a/docs/nginx.txt +++ b/docs/nginx.txt @@ -19,14 +19,10 @@ As an example, let's consider the following download view: * mapped on ``/document//download`` * returns DownloadResponse corresponding to Document's model FileField -* Document storage root is :file:`/var/www/files/`. +* Document storage root is :file:`/var/www/files/` +* FileField's ``upload_to`` is "document". -Configure Document storage: - -.. code-block:: python - - storage = FileSystemStorage(location='var/www/files', - url='/optimized-download') +Files live in ``/var/www/files/document/`` folder. As is, Django is to serve the files, i.e. load chunks into memory and stream them. @@ -82,6 +78,8 @@ Optionally customize configuration (default is "use Nginx's defaults"). .. code-block:: python + NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT = MEDIA_ROOT + NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL = '/optimized-download' NGINX_DOWNLOAD_MIDDLEWARE_EXPIRES = False # Force no expiration. NGINX_DOWNLOAD_MIDDLEWARE_WITH_BUFFERING = False # Force buffering off. NGINX_DOWNLOAD_MIDDLEWARE_LIMIT_RATE = False # Force limit rate off. @@ -104,7 +102,9 @@ In some urls.py: from django_downloadview.nginx import x_accel_redirect - download = x_accel_redirect(ObjectDownloadView.as_view(model=Document)) + download = x_accel_redirect(ObjectDownloadView.as_view(model=Document), + media_root=settings.MEDIA_ROOT, + media_url='/optimized-download') # ... URL patterns using ``download``