Skip to content

Commit

Permalink
Merge pull request #56 from benoitbryon/25-single-backend
Browse files Browse the repository at this point in the history
Closes #25 - Introduced SmartDownloadMiddleware.
  • Loading branch information
benoitbryon committed Nov 6, 2013
2 parents e900c1a + e9d9b3d commit c32313c
Show file tree
Hide file tree
Showing 9 changed files with 174 additions and 106 deletions.
23 changes: 14 additions & 9 deletions CHANGELOG
Expand Up @@ -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:
Expand All @@ -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.
Expand Down
15 changes: 8 additions & 7 deletions demo/demoproject/settings.py
Expand Up @@ -65,18 +65,19 @@
'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'
Expand Down
3 changes: 2 additions & 1 deletion django_downloadview/api.py
Expand Up @@ -8,7 +8,8 @@
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,
Expand Down
85 changes: 68 additions & 17 deletions django_downloadview/middlewares.py
Expand Up @@ -5,11 +5,19 @@
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):
Expand Down Expand Up @@ -70,28 +78,20 @@ def is_download_response(self, response):


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

Expand All @@ -102,6 +102,57 @@ def process_download_response(self, request, response):
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."""

Expand Down
49 changes: 15 additions & 34 deletions django_downloadview/nginx/settings.py
Expand Up @@ -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.
"""
Expand All @@ -22,7 +22,12 @@
'{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.
Expand All @@ -39,10 +44,7 @@
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)
Expand All @@ -61,10 +63,7 @@
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)
Expand All @@ -83,10 +82,7 @@
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)
Expand All @@ -96,18 +92,12 @@
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)
Expand All @@ -117,10 +107,7 @@
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)
Expand All @@ -130,18 +117,12 @@
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)
1 change: 1 addition & 0 deletions django_downloadview/tests/api.py
Expand Up @@ -52,6 +52,7 @@ def test_root_attributes(self):
# Middlewares:
'BaseDownloadMiddleware',
'DownloadDispatcherMiddleware',
'SmartDownloadMiddleware',
# Testing:
'assert_download_response',
'setup_view',
Expand Down
14 changes: 14 additions & 0 deletions django_downloadview/utils.py
Expand Up @@ -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)
24 changes: 12 additions & 12 deletions docs/optimizations/nginx.txt
Expand Up @@ -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:
Expand Down

0 comments on commit c32313c

Please sign in to comment.