Skip to content

Commit

Permalink
Refs #1 - Introduced experimental support for Nginx X-Accel. WORK IN …
Browse files Browse the repository at this point in the history
…PROGRESS.
  • Loading branch information
benoitbryon committed Nov 19, 2012
1 parent 46542cd commit 41e00d3
Show file tree
Hide file tree
Showing 4 changed files with 228 additions and 0 deletions.
101 changes: 101 additions & 0 deletions django_downloadview/nginx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""Let Nginx serve files for increased performance.
See `Nginx X-accel documentation <http://wiki.nginx.org/X-accel>`_.
"""
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)
8 changes: 8 additions & 0 deletions docs/api/django_downloadview.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
----------------------

Expand Down
1 change: 1 addition & 0 deletions docs/index.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Contents
.. toctree::
:maxdepth: 2

nginx
api/modules


Expand Down
118 changes: 118 additions & 0 deletions docs/nginx.txt
Original file line number Diff line number Diff line change
@@ -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/<object-slug>/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/<filename>``.

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

0 comments on commit 41e00d3

Please sign in to comment.