Skip to content

Commit

Permalink
Refs #1 - Experimental support for Nginx X-Accel. Work in progress
Browse files Browse the repository at this point in the history
  • Loading branch information
benoitbryon committed Nov 28, 2012
1 parent 66c83a8 commit 1f8876b
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 23 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')
49 changes: 34 additions & 15 deletions django_downloadview/nginx.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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'
Expand All @@ -52,27 +50,34 @@ 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:
try:
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,
Expand All @@ -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)

Expand Down
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 @@ -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
-------------------------

Expand All @@ -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
-------------------

Expand Down
16 changes: 8 additions & 8 deletions docs/nginx.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,10 @@ 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/`.
* 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.
Expand Down Expand Up @@ -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.
Expand All @@ -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``

Expand Down

0 comments on commit 1f8876b

Please sign in to comment.