From 41e00d312d4776fffdbd3a7ac0b49e5a9d631924 Mon Sep 17 00:00:00 2001 From: Benoit Bryon Date: Mon, 19 Nov 2012 14:41:36 +0100 Subject: [PATCH 1/6] Refs #1 - Introduced experimental support for Nginx X-Accel. WORK IN PROGRESS. --- django_downloadview/nginx.py | 101 ++++++++++++++++++++++++++ docs/api/django_downloadview.txt | 8 +++ docs/index.txt | 1 + docs/nginx.txt | 118 +++++++++++++++++++++++++++++++ 4 files changed, 228 insertions(+) create mode 100644 django_downloadview/nginx.py create mode 100644 docs/nginx.txt diff --git a/django_downloadview/nginx.py b/django_downloadview/nginx.py new file mode 100644 index 0000000..aaf2067 --- /dev/null +++ b/django_downloadview/nginx.py @@ -0,0 +1,101 @@ +"""Let Nginx serve files for increased performance. + +See `Nginx X-accel documentation `_. + +""" +from datetime import datetime, timedelta + +from django.conf import settings +from django.http import HttpResponse + +from django_downloadview.middlewares import BaseDownloadMiddleware +from django_downloadview.decorators import DownloadDecorator + + +#: Default value for X-Accel-Buffering header. +DEFAULT_BUFFERING = None +if not hasattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_BUFFERING'): + setattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_BUFFERING', DEFAULT_BUFFERING) + + +#: Default value for X-Accel-Limit-Rate header. +DEFAULT_LIMIT_RATE = None +if not hasattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_LIMIT_RATE'): + 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, + 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] + self['Content-Disposition'] = 'attachment; filename=%s' % basename + self['X-Accel-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' + if expires: + expire_seconds = timedelta(expires - datetime.now()).seconds + self['X-Accel-Expires'] = expire_seconds + elif expires is not None: # We explicitely want it off. + self['X-Accel-Expires'] = 'off' + if limit_rate is not None: + self['X-Accel-Limit-Rate'] = limit_rate and '%d' % limit_rate \ + or 'off' + + +class BaseXAccelRedirectMiddleware(BaseDownloadMiddleware): + """Looks like a middleware, but configurable.""" + def __init__(self, expires=None, with_buffering=None, limit_rate=None): + """Constructor.""" + self.expires = expires + self.with_buffering = with_buffering + self.limit_rate = limit_rate + + def file_to_url(response): + return response.filename + + def process_download_response(self, request, response): + """Replace DownloadResponse instances by NginxDownloadResponse ones.""" + url = self.file_to_url(response) + if self.expires: + expires = self.expires + else: + try: + expires = response.expires + except AttributeError: + expires = None + return XAccelRedirectResponse(url=url, + content_type=response.content_type, + basename=response.basename, + expires=expires, + with_buffering=self.with_buffering, + limit_rate=self.limit_rate) + + +class XAccelRedirectMiddleware(): + """Apply X-Accel-Redirect globally. + + XAccelRedirectResponseHandler with django settings. + + """ + def __init__(self): + """Use Django settings as configuration.""" + super(XAccelRedirectMiddleware, self).__init__( + expires=settings.NGINX_DOWNLOAD_MIDDLEWARE_EXPIRESS, + with_buffering=settings.NGINX_DOWNLOAD_MIDDLEWARE_WITH_BUFFERING, + limit_rate=settings.NGINX_DOWNLOAD_MIDDLEWARE_LIMIT_RATE) + + +#: Apply BaseXAccelRedirectMiddleware to ``view_func`` response. +#: +#: Proxies additional arguments (``*args``, ``**kwargs``) to +#: :py:meth:`django_downloadview.nginx.BaseXAccelRedirectMiddleware.__init__`: +#: ``expires``, ``with_buffering``, and ``limit_rate``. +x_accel_redirect = DownloadDecorator(BaseXAccelRedirectMiddleware) diff --git a/docs/api/django_downloadview.txt b/docs/api/django_downloadview.txt index 4b70ab3..e54f5cc 100644 --- a/docs/api/django_downloadview.txt +++ b/docs/api/django_downloadview.txt @@ -25,6 +25,14 @@ django_downloadview Package :undoc-members: :show-inheritance: +:mod:`nginx` Module +------------------- + +.. automodule:: django_downloadview.nginx + :members: + :undoc-members: + :show-inheritance: + :mod:`response` Module ---------------------- diff --git a/docs/index.txt b/docs/index.txt index 235d7e8..9fa5743 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -13,6 +13,7 @@ Contents .. toctree:: :maxdepth: 2 + nginx api/modules diff --git a/docs/nginx.txt b/docs/nginx.txt new file mode 100644 index 0000000..69d39ba --- /dev/null +++ b/docs/nginx.txt @@ -0,0 +1,118 @@ +################### +Nginx optimisations +################### + +If you serve Django behind Nginx, then you can delegate the file download +service to Nginx and get increased performance: + +* lower resources used by Python/Django workers ; +* faster download. + +See `Nginx X-accel documentation`_ for details. + + +**************************** +Configure some download view +**************************** + +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/`. + +Configure Document storage: + +.. code-block:: python + + storage = FileSystemStorage(location='var/www/files', + url='/optimized-download') + +As is, Django is to serve the files, i.e. load chunks into memory and stream +them. + +Nginx is much more efficient for the actual streaming. + + +*************** +Configure Nginx +*************** + +See `Nginx X-accel documentation`_ for details. + +In this documentation, let's suppose we have something like this: + +.. code-block:: nginx + + # Will serve /var/www/files/myfile.tar.gz + # When passed URI /protected_files/myfile.tar.gz + location /optimized-download { + internal; + alias /var/www/files; + } + +.. note:: + + ``/optimized-download`` is not available for the client, i.e. users + won't be able to download files via ``/optimized-download/``. + +.. warning:: + + Make sure Nginx can read the files to download! Check permissions. + + +************************************************ +Global delegation, with XAccelRedirectMiddleware +************************************************ + +If you want to delegate all file downloads to Nginx, then use +:py:class:`django_downloadview.nginx.XAccelRedirectMiddleware`. + +Register it in your settings: + +.. code-block:: python + + MIDDLEWARE_CLASSES = ( + # ... + 'django_downloadview.nginx.XAccelRedirectMiddleware', + # ... + ) + +Optionally customize configuration (default is "use Nginx's defaults"). + +.. code-block:: python + + 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. + + +************************************************* +Local delegation, with x_accel_redirect decorator +************************************************* + +If you want to delegate file downloads to Nginx on a per-view basis, then use +:py:func:`django_downloadview.nginx.x_accel_redirect` decorator. + +In some urls.py: + +.. code-block:: python + + # ... import Document and django.core.urls + + from django_downloadview import ObjectDownloadView + from django_downloadview.nginx import x_accel_redirect + + + download = x_accel_redirect(ObjectDownloadView.as_view(model=Document)) + + # ... URL patterns using ``download`` + + +********** +References +********** + +.. target-notes:: + +.. _`Nginx X-accel documentation`: http://wiki.nginx.org/X-accel From 5a031a201153b957b84b524314b7ef3ed631006e Mon Sep 17 00:00:00 2001 From: Benoit Bryon Date: Thu, 22 Nov 2012 09:51:52 +0100 Subject: [PATCH 2/6] Use **kwargs in DownloadMixin.render_to_response() --- django_downloadview/views.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/django_downloadview/views.py b/django_downloadview/views.py index c5419bc..f55f5b7 100644 --- a/django_downloadview/views.py +++ b/django_downloadview/views.py @@ -115,7 +115,7 @@ def get_size(self): self.size = os.path.getsize(self.get_filename()) return self.size - def render_to_response(self, **response_kwargs): + def render_to_response(self, **kwargs): """Returns a response with a file as attachment.""" mime_type = self.get_mime_type() charset = self.get_charset() @@ -132,13 +132,15 @@ def render_to_response(self, **response_kwargs): basename = self.get_basename() encoding = self.get_encoding() wrapper = self.get_file_wrapper() - response = self.response_class(content=wrapper, - content_type=content_type, - content_length=size, - filename=filename, - basename=basename, - content_encoding=encoding, - expires=None) + response_kwargs = {'content': wrapper, + 'content_type': content_type, + 'content_length': size, + 'filename': filename, + 'basename': basename, + 'content_encoding': encoding, + 'expires': None} + response_kwargs.update(kwargs) + response = self.response_class(**response_kwargs) # Do not close the file as response class may need it open: the wrapper # is an iterator on the content of the file. # Garbage collector will close the file. From 450d2f42bb3b8cf3f5a8472e857eb3b0ea04a4fc Mon Sep 17 00:00:00 2001 From: Benoit Bryon Date: Thu, 22 Nov 2012 10:00:36 +0100 Subject: [PATCH 3/6] Minor precision about deserialization in views. --- django_downloadview/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/django_downloadview/views.py b/django_downloadview/views.py index f55f5b7..b258fc4 100644 --- a/django_downloadview/views.py +++ b/django_downloadview/views.py @@ -199,9 +199,9 @@ class ObjectDownloadView(DownloadMixin, BaseDetailView): The main one is ``file_field``. The other arguments are provided for convenience, in case your model holds - some metadata about the file, such as its basename, its modification time, - its MIME type... These fields may be particularly handy if your file - storage is not the local filesystem. + some (deserialized) metadata about the file, such as its basename, its + modification time, its MIME type... These fields may be particularly handy + if your file storage is not the local filesystem. """ #: Name of the model's attribute which contains the file to be streamed. From 66c83a89142b06049feea28946ac643c3093a97d Mon Sep 17 00:00:00 2001 From: Benoit Bryon Date: Thu, 22 Nov 2012 10:29:09 +0100 Subject: [PATCH 4/6] Introduced DownloadResponse.url. Not used yet. --- django_downloadview/response.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/django_downloadview/response.py b/django_downloadview/response.py index 9f6c2c5..565a478 100644 --- a/django_downloadview/response.py +++ b/django_downloadview/response.py @@ -6,7 +6,7 @@ class DownloadResponse(HttpResponse): """File download response.""" def __init__(self, content, content_type, content_length, basename, status=200, content_encoding=None, expires=None, - filename=None): + filename=None, url=None): """Constructor. It differs a bit from HttpResponse constructor. @@ -35,8 +35,20 @@ def __init__(self, content, content_type, content_length, basename, It is used to set the "Expires" header. * ``filename`` is the server-side name of the file. - It may be used by decorators or middlewares to delegate the actual - streaming to a more efficient server (i.e. Nginx, Lighttpd...). + It may be used by decorators or middlewares. + + * ``url`` is the actual URL of the file content. + + * If Django is to serve the file, then ``url`` should be + ``request.get_full_path()``. This should be the default behaviour + when ``url`` is None. + + * If ``url`` is not None and differs from + ``request.get_full_path()``, then it means that the actual download + should be performed at another location. In that case, + DownloadResponse doesn't return a redirection, but ``url`` may be + caught and used by download middlewares or decorators (Nginx, + Lighttpd...). """ super(DownloadResponse, self).__init__(content=content, status=status, From 1f8876ba4fe2db3f5ba046019199c99ae6b2dec9 Mon Sep 17 00:00:00 2001 From: Benoit Bryon Date: Wed, 28 Nov 2012 16:53:20 +0100 Subject: [PATCH 5/6] Refs #1 - Experimental support for Nginx X-Accel. Work in progress --- demo/demoproject/download/tests.py | 23 ++++++++++++++ demo/demoproject/download/urls.py | 2 ++ demo/demoproject/download/views.py | 5 +++ django_downloadview/nginx.py | 49 +++++++++++++++++++++--------- django_downloadview/utils.py | 18 +++++++++++ docs/api/django_downloadview.txt | 16 ++++++++++ docs/nginx.txt | 16 +++++----- 7 files changed, 106 insertions(+), 23 deletions(-) create mode 100644 django_downloadview/utils.py 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`` From 9762025d1ac5ef9248f9347bbe3a7ebefc43c4e9 Mon Sep 17 00:00:00 2001 From: Benoit Bryon Date: Wed, 28 Nov 2012 19:04:13 +0100 Subject: [PATCH 6/6] Refs #1 - Fixed nginx middleware. Populated documentation (examples should be moved to demo project). --- django_downloadview/middlewares.py | 1 + django_downloadview/nginx.py | 17 ++-- docs/api/django_downloadview.txt | 8 -- docs/nginx.txt | 125 +++++++++++++++++++++++++++++ 4 files changed, 138 insertions(+), 13 deletions(-) diff --git a/django_downloadview/middlewares.py b/django_downloadview/middlewares.py index d0c2cbb..31b983f 100644 --- a/django_downloadview/middlewares.py +++ b/django_downloadview/middlewares.py @@ -16,6 +16,7 @@ def process_response(self, request, response): """Call ``process_download_response()`` if ``response`` is download.""" if self.is_download_response(response): return self.process_download_response(request, response) + return response def process_download_response(self, request, response): """Handle file download response.""" diff --git a/django_downloadview/nginx.py b/django_downloadview/nginx.py index 2c8035b..8f69011 100644 --- a/django_downloadview/nginx.py +++ b/django_downloadview/nginx.py @@ -15,15 +15,22 @@ #: Default value for X-Accel-Buffering header. -DEFAULT_BUFFERING = None -if not hasattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_BUFFERING'): - setattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_BUFFERING', DEFAULT_BUFFERING) +DEFAULT_WITH_BUFFERING = None +if not hasattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_WITH_BUFFERING'): + setattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_WITH_BUFFERING', + DEFAULT_WITH_BUFFERING) #: Default value for X-Accel-Limit-Rate header. DEFAULT_LIMIT_RATE = None if not hasattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_LIMIT_RATE'): - setattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_LIMIT', DEFAULT_LIMIT_RATE) + setattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_LIMIT_RATE', DEFAULT_LIMIT_RATE) + + +#: Default value for X-Accel-Limit-Rate header. +DEFAULT_EXPIRES = None +if not hasattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_EXPIRES'): + setattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_EXPIRES', DEFAULT_EXPIRES) class XAccelRedirectResponse(HttpResponse): @@ -84,7 +91,7 @@ def process_download_response(self, request, response): limit_rate=self.limit_rate) -class XAccelRedirectMiddleware(): +class XAccelRedirectMiddleware(BaseXAccelRedirectMiddleware): """Apply X-Accel-Redirect globally. XAccelRedirectResponseHandler with django settings. diff --git a/docs/api/django_downloadview.txt b/docs/api/django_downloadview.txt index cc5f612..c790b25 100644 --- a/docs/api/django_downloadview.txt +++ b/docs/api/django_downloadview.txt @@ -17,14 +17,6 @@ django_downloadview Package :undoc-members: :show-inheritance: -:mod:`file` Module ------------------- - -.. automodule:: django_downloadview.file - :members: - :undoc-members: - :show-inheritance: - :mod:`middlewares` Module ------------------------- diff --git a/docs/nginx.txt b/docs/nginx.txt index 4b11147..6020f60 100644 --- a/docs/nginx.txt +++ b/docs/nginx.txt @@ -109,6 +109,131 @@ In some urls.py: # ... URL patterns using ``download`` +******************** +Sample configuration +******************** + +In this sample configuration... + +* we register files in some "myapp.models.Document" model +* store files in :file:`/var/www/private/` folder +* publish files at ``/download//`` URL +* restrict access to authenticated users with the ``login_required`` decorator +* delegate file download to Nginx, via ``/private/`` internal URL. + +Nginx +===== + +:file:`/etc/nginx/sites-available/default`: + +.. code-block:: nginx + + charset utf-8; + + # Django-powered service. + upstream frontend { + server 127.0.0.1:8000 fail_timeout=0; + } + + server { + listen 80 default; + + # File-download proxy. + # See http://wiki.nginx.org/X-accel + # and https://github.com/benoitbryon/django-downloadview + location /private/ { + internal; + # Location to files on disk. + # See Django's settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT + alias /var/www/private/; + } + + # Proxy to Django-powered frontend. + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + proxy_redirect off; + proxy_pass http://frontend; + } + } + +Django settings +=============== + +:file:`settings.py`: + +.. code-block:: python + + MYAPP_STORAGE_LOCATION = '/var/www/private/' + NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT = MYAPP_STORAGE_LOCATION + MIDDLEWARE_CLASSES = ( + # ... + 'django_downloadview.nginx.XAccelRedirectMiddleware', + # ... + ) + INSTALLED_APPS = ( + # ... + 'myapp', + # ... + ) + +In some model +============= + +:file:`myapp/models.py`: + +.. code-block:: python + + from django.conf import settings + from django.db import models + from django.core.files.storage import FileSystemStorage + + + storage = FileSystemStorage(location=settings.MYAPP_STORAGE_LOCATION) + + + class Document(models.Model): + file = models.ImageField(storage=storage) + +URL patterns +============ + +:file:`myapp/urls.py`: + +.. code-block:: python + + from django.conf.urls import url, url_patterns + from django.contrib.auth.decorators import login_required + + from django_downloadview import ObjectDownloadView + + from myapp.models import Document + + + download = login_required(ObjectDownloadView.as_view(model=Document)) + + url_patterns = ('', + url('^download/(?P[0-9]+/$', download, name='download'), + ) + + +************* +Common issues +************* + +``Unknown charset "utf-8" to override`` +======================================= + +Add ``charset utf-8;`` in your nginx configuration file. + +``open() "path/to/something" failed (2: No such file or directory)`` +==================================================================== + +Check your ``settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT`` in Django +configuration VS ``alias`` in nginx configuration: in a standard configuration, +they should be equal. + + ********** References **********