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