Refs #25 - Introduced SmartDownloadMiddleware: makes it possible to setup several rules given a single optimization backend.

This commit is contained in:
Benoît Bryon 2013-11-07 00:09:30 +01:00
parent e900c1a253
commit e9d9b3dfaa
9 changed files with 175 additions and 107 deletions

View file

@ -21,10 +21,20 @@ documentation.
- Bugfix #49 - Fixed ``content`` assertion in ``assert_download_response``:
checks only response's ``streaming_content`` attribute.
- Feature #50 - Introduced
:class:`~django_downloadview.middlewares.DownloadDispatcherMiddleware` that
iterates over a list of configurable download middlewares. Allows to plug
several download middlewares with different configurations.
- Feature #50 - Introduced ``django_downloadview.DownloadDispatcherMiddleware``
that iterates over a list of configurable download middlewares. Allows to
plug several download middlewares with different configurations.
This middleware is mostly dedicated to internal usage. It is used by
``SmartDownloadMiddleware`` described below.
- Feature #42 - Documentation shows how to stream generated content (yield).
Introduced ``django_downloadview.StringIteratorIO``.
- Refactoring #51 - Dropped support of Python 2.6
- Refactoring #25 - Introduced ``django_downloadview.SmartDownloadMiddleware``
which allows to setup multiple optimization rules for one backend.
Deprecates the following settings related to previous single-and-global
middleware:
@ -35,11 +45,6 @@ documentation.
* ``NGINX_DOWNLOAD_MIDDLEWARE_WITH_BUFFERING``
* ``NGINX_DOWNLOAD_MIDDLEWARE_LIMIT_RATE``
- Feature #42 - Documentation shows how to stream generated content (yield).
Introduced ``django_downloadview.StringIteratorIO``.
- Refactoring #51 - Dropped support of Python 2.6
- Refactoring #52 - ObjectDownloadView now inherits from SingleObjectMixin and
BaseDownloadView (was DownloadMixin and BaseDetailView).
Simplified DownloadMixin.render_to_response() signature.

View file

@ -65,18 +65,19 @@ MIDDLEWARE_CLASSES = [
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django_downloadview.DownloadDispatcherMiddleware'
'django_downloadview.SmartDownloadMiddleware'
]
# Uncomment the following lines to enable global Nginx optimizations.
DOWNLOADVIEW_MIDDLEWARES = (
('default', 'django_downloadview.nginx.XAccelRedirectMiddleware',
{'source_url': '/media/nginx/',
'destination_url': '/nginx-optimized-by-middleware/'}),
)
# Specific configuration for django_downloadview.SmartDownloadMiddleware.
DOWNLOADVIEW_BACKEND = 'django_downloadview.nginx.XAccelRedirectMiddleware'
DOWNLOADVIEW_RULES = [
{'source_url': '/media/nginx/',
'destination_url': '/nginx-optimized-by-middleware/'},
]
# Test/development settings.
DEBUG = True
TEMPLATE_DEBUG = DEBUG
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'

View file

@ -8,7 +8,8 @@ from django_downloadview.files import (StorageFile, # NoQA
from django_downloadview.response import (DownloadResponse, # NoQA
ProxiedDownloadResponse)
from django_downloadview.middlewares import (BaseDownloadMiddleware, # NoQA
DownloadDispatcherMiddleware)
DownloadDispatcherMiddleware,
SmartDownloadMiddleware)
from django_downloadview.views import (PathDownloadView, # NoQA
ObjectDownloadView,
StorageDownloadView,

View file

@ -5,11 +5,19 @@ Download middlewares capture :py:class:`django_downloadview.DownloadResponse`
responses and may replace them with optimized download responses.
"""
import collections
import os
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django_downloadview.response import DownloadResponse
from django_downloadview.utils import import_member
#: Sentinel value to detect whether configuration is to be loaded from Django
#: settings or not.
AUTO_CONFIGURE = object()
def is_download_response(response):
@ -70,28 +78,20 @@ class RealDownloadMiddleware(BaseDownloadMiddleware):
class DownloadDispatcherMiddleware(BaseDownloadMiddleware):
"""Download middleware that dispatches job to several middlewares.
The list of Children middlewares is read in `DOWNLOADVIEW_MIDDLEWARES`
setting.
"""
def __init__(self):
"Download middleware that dispatches job to several middleware instances."
def __init__(self, middlewares=AUTO_CONFIGURE):
#: List of children middlewares.
self.middlewares = []
self.load_middlewares_from_settings()
self.middlewares = middlewares
if self.middlewares is AUTO_CONFIGURE:
self.auto_configure_middlewares()
def load_middlewares_from_settings(self):
def auto_configure_middlewares(self):
"""Populate :attr:`middlewares` from
``settings.DOWNLOADVIEW_MIDDLEWARES``."""
for (key, import_string, kwargs) in getattr(settings,
'DOWNLOADVIEW_MIDDLEWARES',
[]):
if ':' in import_string:
module_string, attr_string = import_string.split(':', 1)
else:
module_string, attr_string = import_string.rsplit('.', 1)
module = __import__(module_string, globals(), locals(),
[attr_string], -1)
factory = getattr(module, attr_string)
factory = import_member(import_string)
middleware = factory(**kwargs)
self.middlewares.append((key, middleware))
@ -102,6 +102,57 @@ class DownloadDispatcherMiddleware(BaseDownloadMiddleware):
return response
class SmartDownloadMiddleware(BaseDownloadMiddleware):
"""Easy to configure download middleware."""
def __init__(self,
backend_factory=AUTO_CONFIGURE,
backend_options=AUTO_CONFIGURE):
"""Constructor."""
#: :class:`DownloadDispatcher` instance that can hold multiple
#: backend instances.
self.dispatcher = DownloadDispatcherMiddleware(middlewares=[])
#: Callable (typically a class) to instanciate backend (typically a
#: :class:`DownloadMiddleware` subclass).
self.backend_factory = backend_factory
if self.backend_factory is AUTO_CONFIGURE:
self.auto_configure_backend_factory()
#: List of positional or keyword arguments to instanciate backend
#: instances.
self.backend_options = backend_options
if self.backend_options is AUTO_CONFIGURE:
self.auto_configure_backend_options()
def auto_configure_backend_factory(self):
"Assign :attr:`backend_factory` from ``settings.DOWNLOADVIEW_BACKEND``"
try:
self.backend_factory = import_member(settings.DOWNLOADVIEW_BACKEND)
except AttributeError:
raise ImproperlyConfigured('SmartDownloadMiddleware requires '
'settings.DOWNLOADVIEW_BACKEND')
def auto_configure_backend_options(self):
"""Populate :attr:`dispatcher` using :attr:`factory` and
``settings.DOWNLOADVIEW_RULES``."""
try:
options_list = settings.DOWNLOADVIEW_RULES
except AttributeError:
raise ImproperlyConfigured('SmartDownloadMiddleware requires '
'settings.DOWNLOADVIEW_RULES')
for key, options in enumerate(options_list):
args = []
kwargs = {}
if isinstance(options, collections.Mapping): # Using kwargs.
kwargs = options
else:
args = options
middleware_instance = self.backend_factory(*args, **kwargs)
self.dispatcher.middlewares.append((key, middleware_instance))
def process_download_response(self, request, response):
"""Use :attr:`dispatcher` to process download response."""
return self.dispatcher.process_download_response(request, response)
class NoRedirectionMatch(Exception):
"""Response object does not match redirection rules."""

View file

@ -4,7 +4,7 @@
.. warning::
These settings are deprecated since version 1.3. You can now provide custom
configuration via `DOWNLOADVIEW_MIDDLEWARES` setting. See :doc:`/settings`
configuration via `DOWNLOADVIEW_BACKEND` setting. See :doc:`/settings`
for details.
"""
@ -22,7 +22,12 @@ if middleware in settings.MIDDLEWARE_CLASSES:
'{middleware} middleware has been renamed as of django-downloadview '
'version 1.3. You may use '
'"django_downloadview.nginx.SingleXAccelRedirectMiddleware" instead, '
'or upgrade to "django_downloadview.DownloadDispatcherMiddleware". ')
'or upgrade to "django_downloadview.SmartDownloadDispatcher". ')
deprecated_msg = 'settings.{deprecated} is deprecated. You should combine ' \
'"django_downloadview.SmartDownloadDispatcher" with ' \
'with DOWNLOADVIEW_BACKEND and DOWNLOADVIEW_RULES instead.'
#: Default value for X-Accel-Buffering header.
@ -39,10 +44,7 @@ if middleware in settings.MIDDLEWARE_CLASSES:
DEFAULT_WITH_BUFFERING = None
setting_name = 'NGINX_DOWNLOAD_MIDDLEWARE_WITH_BUFFERING'
if hasattr(settings, setting_name):
warnings.warn('settings.{deprecated} is deprecated. You should combine '
'"django_downloadview.DownloadDispatcherMiddleware" with '
'with DOWNLOADVIEW_MIDDLEWARES instead.'.format(
deprecated=setting_name),
warnings.warn(deprecated_msg.format(deprecated=setting_name),
DeprecationWarning)
if not hasattr(settings, setting_name):
setattr(settings, setting_name, DEFAULT_WITH_BUFFERING)
@ -61,10 +63,7 @@ if not hasattr(settings, setting_name):
DEFAULT_LIMIT_RATE = None
setting_name = 'NGINX_DOWNLOAD_MIDDLEWARE_LIMIT_RATE'
if hasattr(settings, setting_name):
warnings.warn('settings.{deprecated} is deprecated. You should combine '
'"django_downloadview.DownloadDispatcherMiddleware" with '
'with DOWNLOADVIEW_MIDDLEWARES instead.'.format(
deprecated=setting_name),
warnings.warn(deprecated_msg.format(deprecated=setting_name),
DeprecationWarning)
if not hasattr(settings, setting_name):
setattr(settings, setting_name, DEFAULT_LIMIT_RATE)
@ -83,10 +82,7 @@ if not hasattr(settings, setting_name):
DEFAULT_EXPIRES = None
setting_name = 'NGINX_DOWNLOAD_MIDDLEWARE_EXPIRES'
if hasattr(settings, setting_name):
warnings.warn('settings.{deprecated} is deprecated. You should combine '
'"django_downloadview.DownloadDispatcherMiddleware" with '
'with DOWNLOADVIEW_MIDDLEWARES instead.'.format(
deprecated=setting_name),
warnings.warn(deprecated_msg.format(deprecated=setting_name),
DeprecationWarning)
if not hasattr(settings, setting_name):
setattr(settings, setting_name, DEFAULT_EXPIRES)
@ -96,18 +92,12 @@ if not hasattr(settings, setting_name):
DEFAULT_SOURCE_DIR = settings.MEDIA_ROOT
setting_name = 'NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT'
if hasattr(settings, setting_name):
warnings.warn('settings.{deprecated} is deprecated. You should combine '
'"django_downloadview.DownloadDispatcherMiddleware" with '
'with DOWNLOADVIEW_MIDDLEWARES instead.'.format(
deprecated=setting_name),
warnings.warn(deprecated_msg.format(deprecated=setting_name),
DeprecationWarning)
DEFAULT_SOURCE_DIR = settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT
setting_name = 'NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR'
if hasattr(settings, setting_name):
warnings.warn('settings.{deprecated} is deprecated. You should combine '
'"django_downloadview.DownloadDispatcherMiddleware" with '
'with DOWNLOADVIEW_MIDDLEWARES instead.'.format(
deprecated=setting_name),
warnings.warn(deprecated_msg.format(deprecated=setting_name),
DeprecationWarning)
if not hasattr(settings, setting_name):
setattr(settings, setting_name, DEFAULT_SOURCE_DIR)
@ -117,10 +107,7 @@ if not hasattr(settings, setting_name):
DEFAULT_SOURCE_URL = settings.MEDIA_URL
setting_name = 'NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_URL'
if hasattr(settings, setting_name):
warnings.warn('settings.{deprecated} is deprecated. You should combine '
'"django_downloadview.DownloadDispatcherMiddleware" with '
'with DOWNLOADVIEW_MIDDLEWARES instead.'.format(
deprecated=setting_name),
warnings.warn(deprecated_msg.format(deprecated=setting_name),
DeprecationWarning)
if not hasattr(settings, setting_name):
setattr(settings, setting_name, DEFAULT_SOURCE_URL)
@ -130,18 +117,12 @@ if not hasattr(settings, setting_name):
DEFAULT_DESTINATION_URL = None
setting_name = 'NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL'
if hasattr(settings, setting_name):
warnings.warn('settings.{deprecated} is deprecated. You should combine '
'"django_downloadview.DownloadDispatcherMiddleware" with '
'with DOWNLOADVIEW_MIDDLEWARES instead.'.format(
deprecated=setting_name),
warnings.warn(deprecated_msg.format(deprecated=setting_name),
DeprecationWarning)
DEFAULT_SOURCE_DIR = settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL
setting_name = 'NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL'
if hasattr(settings, setting_name):
warnings.warn('settings.{deprecated} is deprecated. You should combine '
'"django_downloadview.DownloadDispatcherMiddleware" with '
'with DOWNLOADVIEW_MIDDLEWARES instead.'.format(
deprecated=setting_name),
warnings.warn(deprecated_msg.format(deprecated=setting_name),
DeprecationWarning)
if not hasattr(settings, setting_name):
setattr(settings, setting_name, DEFAULT_DESTINATION_URL)

View file

@ -52,6 +52,7 @@ class APITestCase(unittest.TestCase):
# Middlewares:
'BaseDownloadMiddleware',
'DownloadDispatcherMiddleware',
'SmartDownloadMiddleware',
# Testing:
'assert_download_response',
'setup_view',

View file

@ -31,3 +31,17 @@ def url_basename(url, content_type):
"""
return url.split('/')[-1]
def import_member(import_string):
"""Import one member of Python module by path.
>>> import os.path
>>> imported = import_member('os.path.supports_unicode_filenames')
>>> os.path.supports_unicode_filenames is imported
True
"""
module_name, factory_name = str(import_string).rsplit('.', 1)
module = __import__(module_name, globals(), locals(), [factory_name], -1)
return getattr(module, factory_name)

View file

@ -36,31 +36,31 @@ implemented by storage. Let's setup an optimization rule based on that URL.
Setup XAccelRedirect middlewares
********************************
Make sure ``django_downloadview.DownloadDispatcherMiddleware`` is in
Make sure ``django_downloadview.SmartDownloadMiddleware`` is in
``MIDDLEWARE_CLASSES`` of your `Django` settings.
Example:
.. literalinclude:: /../demo/demoproject/settings.py
:language: python
:lines: 61-68
:lines: 62-69
Then register as many
:class:`~django_downloadview.nginx.middlewares.XAccelRedirectMiddleware`
instances as you wish in ``DOWNLOADVIEW_MIDDLEWARES``.
Then set ``django_downloadview.nginx.XAccelRedirectMiddleware`` as
``DOWNLOADVIEW_BACKEND``:
.. literalinclude:: /../demo/demoproject/settings.py
:language: python
:lines: 72-76
:lines: 73
The first item is an identifier.
Then register as many ``DOWNLOADVIEW_RULES`` as you wish:
The second item is the import path of
:class:`~django_downloadview.nginx.middlewares.XAccelRedirectMiddleware` class.
.. literalinclude:: /../demo/demoproject/settings.py
:language: python
:lines: 74-77
The third item 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_url``.
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_url``.
.. autoclass:: django_downloadview.nginx.middlewares.XAccelRedirectMiddleware
:members:

View file

@ -18,7 +18,7 @@ MIDDLEWARE_CLASSES
******************
If you plan to setup reverse-proxy optimizations, add
``django_downloadview.DownloadDispatcherMiddleware`` to ``MIDDLEWARE_CLASSES``.
``django_downloadview.SmartDownloadMiddleware`` to ``MIDDLEWARE_CLASSES``.
It is a response middleware. Move it after middlewares that compute the
response content such as gzip middleware.
@ -26,40 +26,54 @@ Example:
.. literalinclude:: /../demo/demoproject/settings.py
:language: python
:lines: 61-68
:lines: 62-69
************************
DOWNLOADVIEW_MIDDLEWARES
************************
********************
DOWNLOADVIEW_BACKEND
********************
:default: []
If you plan to setup reverse-proxy :doc:`optimizations </optimizations/index>`,
setup ``DOWNLOADVIEW_MIDDLEWARES`` value. This setting is used by
:py:class:`~django_downloadview.middlewares.DownloadDispatcherMiddleware`.
It is the list of handlers that will be given the opportunity to capture
download responses and convert them to internal redirects for use with
reverse-proxies.
The list expects items ``(id, path, options)`` such as:
* ``id`` is an identifier
* ``path`` is the import path of some download middleware factory (typically a
class).
* ``options`` is a dictionary of keyword arguments passed to the middleware
factory.
This setting is used by
:class:`~django_downloadview.middlewares.SmartDownloadMiddleware`.
It is the import string of a callable (typically a class) of an optimization
backend (typically a :class:`~django_downloadview.BaseDownloadMiddleware`
subclass).
Example:
.. literalinclude:: /../demo/demoproject/settings.py
:language: python
:lines: 72-76
:lines: 73
See :doc:`/optimizations/index` for details about middlewares and their
options.
See :doc:`/optimizations/index` for a list of available backends (middlewares).
.. note::
When ``django_downloadview.SmartDownloadMiddleware`` is in your
``MIDDLEWARE_CLASSES``, this setting must be explicitely configured (no default
value). Else, you can ignore this setting.
You can register several middlewares. It allows you to setup several
conversion rules with distinct source/destination patterns.
******************
DOWNLOADVIEW_RULES
******************
This setting is used by
:class:`~django_downloadview.middlewares.SmartDownloadMiddleware`.
It is a list of positional arguments or keyword arguments that will be used to
instanciate class mentioned as ``DOWNLOADVIEW_BACKEND``.
Each item in the list can be either a list of positional arguments, or a
dictionary of keyword arguments. One item cannot contain both positional and
keyword arguments.
Here is an example containing one rule using keyword arguments:
.. literalinclude:: /../demo/demoproject/settings.py
:language: python
:lines: 74-77
See :doc:`/optimizations/index` for details about builtin backends
(middlewares) and their options.
When ``django_downloadview.SmartDownloadMiddleware`` is in your
``MIDDLEWARE_CLASSES``, this setting must be explicitely configured (no default
value). Else, you can ignore this setting.