diff --git a/CHANGELOG b/CHANGELOG index 556abe1..c90e236 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -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. diff --git a/demo/demoproject/settings.py b/demo/demoproject/settings.py index 86e6110..9782567 100755 --- a/demo/demoproject/settings.py +++ b/demo/demoproject/settings.py @@ -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' diff --git a/django_downloadview/api.py b/django_downloadview/api.py index 5428535..e357621 100644 --- a/django_downloadview/api.py +++ b/django_downloadview/api.py @@ -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, diff --git a/django_downloadview/middlewares.py b/django_downloadview/middlewares.py index 1ad1f9b..b02a24a 100644 --- a/django_downloadview/middlewares.py +++ b/django_downloadview/middlewares.py @@ -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.""" diff --git a/django_downloadview/nginx/settings.py b/django_downloadview/nginx/settings.py index 9c5b860..63dd551 100644 --- a/django_downloadview/nginx/settings.py +++ b/django_downloadview/nginx/settings.py @@ -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) diff --git a/django_downloadview/tests/api.py b/django_downloadview/tests/api.py index 40bdbeb..60c13e8 100644 --- a/django_downloadview/tests/api.py +++ b/django_downloadview/tests/api.py @@ -52,6 +52,7 @@ class APITestCase(unittest.TestCase): # Middlewares: 'BaseDownloadMiddleware', 'DownloadDispatcherMiddleware', + 'SmartDownloadMiddleware', # Testing: 'assert_download_response', 'setup_view', diff --git a/django_downloadview/utils.py b/django_downloadview/utils.py index 37f5bbf..f5b3326 100644 --- a/django_downloadview/utils.py +++ b/django_downloadview/utils.py @@ -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) diff --git a/docs/optimizations/nginx.txt b/docs/optimizations/nginx.txt index 8430fb0..d4bc621 100644 --- a/docs/optimizations/nginx.txt +++ b/docs/optimizations/nginx.txt @@ -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: diff --git a/docs/settings.txt b/docs/settings.txt index d289d7d..b5b218c 100644 --- a/docs/settings.txt +++ b/docs/settings.txt @@ -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 `, -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.