Skip to content

Commit

Permalink
Merge pull request #6 from benoitbryon/1-nginx
Browse files Browse the repository at this point in the history
Closes #1. Introducing Nginx optimisations. Should be enough for a start.
  • Loading branch information
benoitbryon committed Nov 28, 2012
2 parents e5a36a4 + 9762025 commit 7738d1c
Show file tree
Hide file tree
Showing 9 changed files with 436 additions and 0 deletions.
23 changes: 23 additions & 0 deletions demo/demoproject/download/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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')
2 changes: 2 additions & 0 deletions demo/demoproject/download/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@
name='download_hello_world'),
url(r'^document/(?P<slug>[a-zA-Z0-9_-]+)/$', 'download_document',
name='download_document'),
url(r'^document-nginx/(?P<slug>[a-zA-Z0-9_-]+)/$',
'download_document_nginx', name='download_document_nginx'),
)
5 changes: 5 additions & 0 deletions demo/demoproject/download/views.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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')
1 change: 1 addition & 0 deletions django_downloadview/middlewares.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
127 changes: 127 additions & 0 deletions django_downloadview/nginx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
"""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.core.exceptions import ImproperlyConfigured
from django.http import HttpResponse

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.
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_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):
"""Http response that delegate serving file to Nginx."""
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 redirect_url.split('/')[-1]
self['Content-Disposition'] = 'attachment; filename=%s' % basename
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'
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, 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 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."""
redirect_url = self.get_redirect_url(response)
if self.expires:
expires = self.expires
else:
try:
expires = response.expires
except AttributeError:
expires = None
return XAccelRedirectResponse(redirect_url=redirect_url,
content_type=response['Content-Type'],
basename=response.basename,
expires=expires,
with_buffering=self.with_buffering,
limit_rate=self.limit_rate)


class XAccelRedirectMiddleware(BaseXAccelRedirectMiddleware):
"""Apply X-Accel-Redirect globally.
XAccelRedirectResponseHandler with django settings.
"""
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__(
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)


#: 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)
18 changes: 18 additions & 0 deletions django_downloadview/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""Utility functions."""
import re


charset_pattern = re.compile(r'charset=(?P<charset>.+)$', 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')
16 changes: 16 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 All @@ -33,6 +41,14 @@ django_downloadview Package
:undoc-members:
:show-inheritance:

:mod:`utils` Module
-------------------

.. automodule:: django_downloadview.utils
:members:
:undoc-members:
:show-inheritance:

:mod:`views` 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

0 comments on commit 7738d1c

Please sign in to comment.