Refs #2 - Introduced support of Lighttpd's X-Sendfile (mostly copied from django_downloadview.apache).

This commit is contained in:
Benoît Bryon 2013-11-28 23:02:40 +01:00
parent b5191c6a6f
commit e33a8165ef
19 changed files with 383 additions and 22 deletions

View file

@ -10,6 +10,7 @@ future releases, check `milestones`_ and :doc:`/about/vision`.
X-Sendfile support.
- Feature #2 - Introduced support of Lighttpd's x-Sendfile.
- Feature #36 - Introduced support of Apache's mod_xsendfile.

View file

@ -0,0 +1 @@
"""Lighttpd optimizations."""

View file

@ -0,0 +1 @@
"""Required to make a Django application."""

View file

@ -0,0 +1,43 @@
import os
from django.core.files.base import ContentFile
from django.core.urlresolvers import reverse
import django.test
from django_downloadview.lighttpd import assert_x_sendfile
from demoproject.lighttpd.views import storage, storage_dir
def setup_file():
if not os.path.exists(storage_dir):
os.makedirs(storage_dir)
storage.save('hello-world.txt', ContentFile(u'Hello world!\n'))
class OptimizedByMiddlewareTestCase(django.test.TestCase):
def test_response(self):
"""'lighttpd:optimized_by_middleware' returns X-Sendfile response."""
setup_file()
url = reverse('lighttpd:optimized_by_middleware')
response = self.client.get(url)
assert_x_sendfile(
self,
response,
content_type="text/plain; charset=utf-8",
basename="hello-world.txt",
file_path="/lighttpd-optimized-by-middleware/hello-world.txt")
class OptimizedByDecoratorTestCase(django.test.TestCase):
def test_response(self):
"""'lighttpd:optimized_by_decorator' returns X-Sendfile response."""
setup_file()
url = reverse('lighttpd:optimized_by_decorator')
response = self.client.get(url)
assert_x_sendfile(
self,
response,
content_type="text/plain; charset=utf-8",
basename="hello-world.txt",
file_path="/lighttpd-optimized-by-decorator/hello-world.txt")

View file

@ -0,0 +1,13 @@
"""URL mapping."""
from django.conf.urls import patterns, url
urlpatterns = patterns(
'demoproject.lighttpd.views',
url(r'^optimized-by-middleware/$',
'optimized_by_middleware',
name='optimized_by_middleware'),
url(r'^optimized-by-decorator/$',
'optimized_by_decorator',
name='optimized_by_decorator'),
)

View file

@ -0,0 +1,22 @@
import os
from django.conf import settings
from django.core.files.storage import FileSystemStorage
from django_downloadview import StorageDownloadView
from django_downloadview.lighttpd import x_sendfile
storage_dir = os.path.join(settings.MEDIA_ROOT, 'lighttpd')
storage = FileSystemStorage(location=storage_dir,
base_url=''.join([settings.MEDIA_URL, 'lighttpd/']))
optimized_by_middleware = StorageDownloadView.as_view(storage=storage,
path='hello-world.txt')
optimized_by_decorator = x_sendfile(
StorageDownloadView.as_view(storage=storage, path='hello-world.txt'),
source_url=storage.base_url,
destination_dir='/lighttpd-optimized-by-decorator/')

View file

@ -53,6 +53,7 @@ INSTALLED_APPS = (
'demoproject.virtual', # Demo around VirtualDownloadView
'demoproject.nginx', # Sample optimizations for Nginx X-Accel.
'demoproject.apache', # Sample optimizations for Apache X-Sendfile.
'demoproject.lighttpd', # Sample optimizations for Lighttpd X-Sendfile.
# For test purposes. The demo project is part of django-downloadview
# test suite.
'django_nose',
@ -74,6 +75,7 @@ MIDDLEWARE_CLASSES = [
DOWNLOADVIEW_BACKEND = 'django_downloadview.nginx.XAccelRedirectMiddleware'
"""Could also be:
DOWNLOADVIEW_BACKEND = 'django_downloadview.apache.XSendfileMiddleware'
DOWNLOADVIEW_BACKEND = 'django_downloadview.lighttpd.XSendfileMiddleware'
"""
DOWNLOADVIEW_RULES = [
{
@ -89,6 +91,15 @@ DOWNLOADVIEW_RULES = [
# demonstrate usage of several backends.
'backend': 'django_downloadview.apache.XSendfileMiddleware',
},
{
'source_url': '/media/lighttpd/',
'destination_dir': '/lighttpd-optimized-by-middleware/',
# Bypass global default backend with additional argument "backend".
# Notice that in general use case, ``DOWNLOADVIEW_BACKEND`` should be
# enough. Here, the django_downloadview demo project needs to
# demonstrate usage of several backends.
'backend': 'django_downloadview.lighttpd.XSendfileMiddleware',
},
]

View file

@ -35,6 +35,10 @@ urlpatterns = patterns(
url(r'^apache/', include('demoproject.apache.urls',
app_name='apache',
namespace='apache')),
# Lighttpd optimizations.
url(r'^lighttpd/', include('demoproject.lighttpd.urls',
app_name='lighttpd',
namespace='lighttpd')),
# An informative homepage.
url(r'$', home, name='home')
)

View file

@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
"""Optimizations for Lighttpd.
See also `documentation of X-Sendfile for Lighttpd
<http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file>`_ and
:doc:`narrative documentation about Lighttpd optimizations
</optimizations/lighttpd>`.
"""
# API shortcuts.
from django_downloadview.lighttpd.decorators import x_sendfile # NoQA
from django_downloadview.lighttpd.response import XSendfileResponse # NoQA
from django_downloadview.lighttpd.tests import assert_x_sendfile # NoQA
from django_downloadview.lighttpd.middlewares import XSendfileMiddleware # NoQA

View file

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
"""Decorators to apply Lighttpd X-Sendfile on a specific view."""
from django_downloadview.decorators import DownloadDecorator
from django_downloadview.lighttpd.middlewares import XSendfileMiddleware
def x_sendfile(view_func, *args, **kwargs):
"""Apply
:class:`~django_downloadview.lighttpd.middlewares.XSendfileMiddleware` to
``view_func``.
Proxies (``*args``, ``**kwargs``) to middleware constructor.
"""
decorator = DownloadDecorator(XSendfileMiddleware)
return decorator(view_func, *args, **kwargs)

View file

@ -0,0 +1,30 @@
from django_downloadview.lighttpd.response import XSendfileResponse
from django_downloadview.middlewares import (ProxiedDownloadMiddleware,
NoRedirectionMatch)
class XSendfileMiddleware(ProxiedDownloadMiddleware):
"""Configurable middleware, for use in decorators or in global middlewares.
Standard Django middlewares are configured globally via settings. Instances
of this class are to be configured individually. It makes it possible to
use this class as the factory in
:py:class:`django_downloadview.decorators.DownloadDecorator`.
"""
def __init__(self, source_dir=None, source_url=None, destination_dir=None):
"""Constructor."""
super(XSendfileMiddleware, self).__init__(source_dir,
source_url,
destination_dir)
def process_download_response(self, request, response):
"""Replace DownloadResponse instances by XSendfileResponse ones."""
try:
redirect_url = self.get_redirect_url(response)
except NoRedirectionMatch:
return response
return XSendfileResponse(file_path=redirect_url,
content_type=response['Content-Type'],
basename=response.basename,
attachment=response.attachment)

View file

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
"""Lighttpd's specific responses."""
import os.path
from django_downloadview.response import (ProxiedDownloadResponse,
content_disposition)
class XSendfileResponse(ProxiedDownloadResponse):
"Delegates serving file to Lighttpd via X-Sendfile header."
def __init__(self, file_path, content_type, basename=None,
attachment=True):
"""Return a HttpResponse with headers for Lighttpd X-Sendfile."""
super(XSendfileResponse, self).__init__(content_type=content_type)
if attachment:
self.basename = basename or os.path.basename(file_path)
self['Content-Disposition'] = content_disposition(self.basename)
self['X-Sendfile'] = file_path

View file

@ -0,0 +1,28 @@
import django_downloadview.apache.tests
from django_downloadview.lighttpd.response import XSendfileResponse
class XSendfileValidator(django_downloadview.apache.tests.XSendfileValidator):
"""Utility class to validate XSendfileResponse instances.
See also :py:func:`assert_x_sendfile` shortcut function.
"""
def assert_x_sendfile_response(self, test_case, response):
test_case.assertTrue(isinstance(response, XSendfileResponse))
def assert_x_sendfile(test_case, response, **assertions):
"""Make ``test_case`` assert that ``response`` is a XSendfileResponse.
Optional ``assertions`` dictionary can be used to check additional items:
* ``basename``: the basename of the file in the response.
* ``content_type``: the value of "Content-Type" header.
* ``file_path``: the value of "X-Sendfile" header.
"""
validator = XSendfileValidator()
return validator(test_case, response, **assertions)

View file

@ -70,6 +70,24 @@ class APITestCase(unittest.TestCase):
'assert_x_accel_redirect']
self.assert_module_attributes('django_downloadview.nginx', api)
def test_apache_attributes(self):
"""Apache-related API is exposed in django_downloadview.apache."""
api = [
'XSendfileResponse',
'XSendfileMiddleware',
'x_sendfile',
'assert_x_sendfile']
self.assert_module_attributes('django_downloadview.apache', api)
def test_lighttpd_attributes(self):
"""Lighttpd-related API is exposed in django_downloadview.lighttpd."""
api = [
'XSendfileResponse',
'XSendfileMiddleware',
'x_sendfile',
'assert_x_sendfile']
self.assert_module_attributes('django_downloadview.lighttpd', api)
class DeprecatedAPITestCase(django.test.SimpleTestCase):
"""Make sure using deprecated items raise DeprecationWarning."""

View file

@ -51,20 +51,20 @@ Example:
.. literalinclude:: /../demo/demoproject/settings.py
:language: python
:lines: 63-70
:lines: 64-71
Then set ``django_downloadview.apache.XSendfileMiddleware`` as
``DOWNLOADVIEW_BACKEND``:
.. literalinclude:: /../demo/demoproject/settings.py
:language: python
:lines: 76
:lines: 77
Then register as many ``DOWNLOADVIEW_RULES`` as you wish:
.. literalinclude:: /../demo/demoproject/settings.py
:language: python
:lines: 78, 79-82, 92
:lines: 80, 85-87, 93, 103
Each item in ``DOWNLOADVIEW_RULES`` is a dictionary of keyword arguments passed
to the middleware factory. In the example above, we capture responses by

View file

@ -19,19 +19,19 @@ Supported features grid
Supported features depend on backend. Given the file you want to stream, the
backend may or may not be able to handle it:
+-----------------------+-------------------------+-------------------------+
| View / File | :doc:`nginx` | :doc:`apache` |
+=======================+=========================+=========================+
| :doc:`/views/path` | Yes, local filesystem. | Yes, local filesystem. |
+-----------------------+-------------------------+-------------------------+
| :doc:`/views/storage` | Yes, local and remote. | Yes, local filesystem. |
+-----------------------+-------------------------+-------------------------+
| :doc:`/views/object` | Yes, local and remote. | Yes, local filesystem. |
+-----------------------+-------------------------+-------------------------+
| :doc:`/views/http` | Yes. | No. |
+-----------------------+-------------------------+-------------------------+
| :doc:`/views/virtual` | No. | No. |
+-----------------------+-------------------------+-------------------------+
+-----------------------+-------------------------+-------------------------+-------------------------+
| View / File | :doc:`nginx` | :doc:`apache` | :doc:`lighttpd` |
+=======================+=========================+=========================+=========================+
| :doc:`/views/path` | Yes, local filesystem. | Yes, local filesystem. | Yes, local filesystem. |
+-----------------------+-------------------------+-------------------------+-------------------------+
| :doc:`/views/storage` | Yes, local and remote. | Yes, local filesystem. | Yes, local filesystem. |
+-----------------------+-------------------------+-------------------------+-------------------------+
| :doc:`/views/object` | Yes, local and remote. | Yes, local filesystem. | Yes, local filesystem. |
+-----------------------+-------------------------+-------------------------+-------------------------+
| :doc:`/views/http` | Yes. | No. | No. |
+-----------------------+-------------------------+-------------------------+-------------------------+
| :doc:`/views/virtual` | No. | No. | No. |
+-----------------------+-------------------------+-------------------------+-------------------------+
As an example, :doc:`Nginx X-Accel </optimizations/nginx>` handles URL for
internal redirects, so it can manage
@ -77,6 +77,7 @@ Here are optimizations builtin `django_downloadview`:
nginx
apache
lighttpd
.. note:: If you need support for additional optimizations, `tell us`_!

View file

@ -0,0 +1,140 @@
########
Lighttpd
########
If you serve Django behind `Lighttpd`, then you can delegate the file streaming
to `Lighttpd` and get increased performance:
* lower resources used by Python/Django workers ;
* faster download.
See `Lighttpd X-Sendfile documentation`_ for details.
.. note::
Currently, `django_downloadview` supports ``X-Sendfile``, but not
``X-Sendfile2``. If you need ``X-Sendfile2`` or know how to handle it,
check `X-Sendfile2 feature request on django_downloadview's bugtracker`_.
*****************
Known limitations
*****************
* Lighttpd needs access to the resource by path on local filesystem.
* Thus only files that live on local filesystem can be streamed by Lighttpd.
************
Given a view
************
Let's consider the following view:
.. literalinclude:: /../demo/demoproject/lighttpd/views.py
:language: python
:lines: 1-6, 8-16
What is important here is that the files will have an ``url`` property
implemented by storage. Let's setup an optimization rule based on that URL.
.. note::
It is generally easier to setup rules based on URL rather than based on
name in filesystem. This is because path is generally relative to storage,
whereas URL usually contains some storage identifier, i.e. it is easier to
target a specific location by URL rather than by filesystem name.
***************************
Setup XSendfile middlewares
***************************
Make sure ``django_downloadview.SmartDownloadMiddleware`` is in
``MIDDLEWARE_CLASSES`` of your `Django` settings.
Example:
.. literalinclude:: /../demo/demoproject/settings.py
:language: python
:lines: 64-71
Then set ``django_downloadview.lighttpd.XSendfileMiddleware`` as
``DOWNLOADVIEW_BACKEND``:
.. literalinclude:: /../demo/demoproject/settings.py
:language: python
:lines: 78
Then register as many ``DOWNLOADVIEW_RULES`` as you wish:
.. literalinclude:: /../demo/demoproject/settings.py
:language: python
:lines: 80, 94-96, 102, 103
Each item in ``DOWNLOADVIEW_RULES`` is a dictionary of keyword arguments passed
to the middleware factory. In the example above, we capture responses by
``source_url`` and convert them to internal redirects to ``destination_dir``.
.. autoclass:: django_downloadview.lighttpd.middlewares.XSendfileMiddleware
:members:
:inherited-members:
:undoc-members:
:show-inheritance:
:member-order: bysource
****************************************
Per-view setup with x_sendfile decorator
****************************************
Middlewares should be enough for most use cases, but you may want per-view
configuration. For `Lighttpd`, there is ``x_sendfile``:
.. autofunction:: django_downloadview.lighttpd.decorators.x_sendfile
As an example:
.. literalinclude:: /../demo/demoproject/lighttpd/views.py
:language: python
:lines: 1-7, 17-
*************************************
Test responses with assert_x_sendfile
*************************************
Use :func:`~django_downloadview.lighttpd.decorators.assert_x_sendfile`
function as a shortcut in your tests.
.. literalinclude:: /../demo/demoproject/lighttpd/tests.py
:language: python
.. autofunction:: django_downloadview.lighttpd.tests.assert_x_sendfile
The tests above assert the `Django` part is OK. Now let's configure `Lighttpd`.
**************
Setup Lighttpd
**************
See `Lighttpd X-Sendfile documentation`_ for details.
*********************************************
Assert everything goes fine with healthchecks
*********************************************
:doc:`Healthchecks </healthchecks>` are the best way to check the complete
setup.
.. rubric:: References
.. target-notes::
.. _`Lighttpd X-Sendfile documentation`:
http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file
.. _`X-Sendfile2 feature request on django_downloadview's bugtracker`:
https://github.com/benoitbryon/django-downloadview/issues/67

View file

@ -52,20 +52,20 @@ Example:
.. literalinclude:: /../demo/demoproject/settings.py
:language: python
:lines: 63-70
:lines: 64-71
Then set ``django_downloadview.nginx.XAccelRedirectMiddleware`` as
``DOWNLOADVIEW_BACKEND``:
.. literalinclude:: /../demo/demoproject/settings.py
:language: python
:lines: 74
:lines: 75
Then register as many ``DOWNLOADVIEW_RULES`` as you wish:
.. literalinclude:: /../demo/demoproject/settings.py
:language: python
:lines: 78, 79-82, 92
:lines: 80, 81-84, 103
Each item in ``DOWNLOADVIEW_RULES`` is a dictionary of keyword arguments passed
to the middleware factory. In the example above, we capture responses by

View file

@ -26,7 +26,7 @@ Example:
.. literalinclude:: /../demo/demoproject/settings.py
:language: python
:lines: 63-70
:lines: 64-71
********************
@ -43,7 +43,7 @@ Example:
.. literalinclude:: /../demo/demoproject/settings.py
:language: python
:lines: 74
:lines: 75
See :doc:`/optimizations/index` for a list of available backends (middlewares).
@ -69,7 +69,7 @@ Here is an example containing one rule using keyword arguments:
.. literalinclude:: /../demo/demoproject/settings.py
:language: python
:lines: 78, 79-82, 92
:lines: 80, 81-84, 103
See :doc:`/optimizations/index` for details about builtin backends
(middlewares) and their options.