mirror of
https://github.com/jazzband/django-downloadview.git
synced 2026-03-16 22:40:25 +00:00
232 lines
8.3 KiB
Python
232 lines
8.3 KiB
Python
"""Base material for download middlewares.
|
|
|
|
Download middlewares capture :py:class:`django_downloadview.DownloadResponse`
|
|
responses and may replace them with optimized download responses.
|
|
|
|
"""
|
|
|
|
import collections.abc
|
|
import copy
|
|
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):
|
|
"""Return ``True`` if ``response`` is a download response.
|
|
|
|
Current implementation returns True if ``response`` is an instance of
|
|
:py:class:`django_downloadview.response.DownloadResponse`.
|
|
|
|
"""
|
|
return isinstance(response, DownloadResponse)
|
|
|
|
|
|
class BaseDownloadMiddleware:
|
|
"""Base (abstract) Django middleware that handles download responses.
|
|
|
|
Subclasses **must** implement :py:meth:`process_download_response` method.
|
|
|
|
"""
|
|
|
|
def __init__(self, get_response):
|
|
self.get_response = get_response
|
|
|
|
def __call__(self, request):
|
|
response = self.get_response(request)
|
|
return self.process_response(request, response)
|
|
|
|
def is_download_response(self, response):
|
|
"""Return True if ``response`` can be considered as a file download.
|
|
|
|
By default, this method uses
|
|
:py:func:`django_downloadview.middlewares.is_download_response`.
|
|
Override this method if you want a different behaviour.
|
|
|
|
"""
|
|
return is_download_response(response)
|
|
|
|
def process_response(self, request, response):
|
|
"""Call `process_download_response()` if ``response`` is download."""
|
|
if self.is_download_response(response):
|
|
return self.process_download_response(request, response)
|
|
return response
|
|
|
|
def process_download_response(self, request, response):
|
|
"""Handle file download response."""
|
|
raise NotImplementedError()
|
|
|
|
|
|
class RealDownloadMiddleware(BaseDownloadMiddleware):
|
|
"""Download middleware that cannot handle virtual files."""
|
|
|
|
def is_download_response(self, response):
|
|
"""Return True for DownloadResponse, except for "virtual" files.
|
|
|
|
This implementation cannot handle files that live in memory or which
|
|
are to be dynamically iterated over. So, we capture only responses
|
|
whose file attribute have either an URL or a file name.
|
|
|
|
"""
|
|
return super().is_download_response(response) and bool(
|
|
getattr(response.file, "url", None) or getattr(response.file, "name", None)
|
|
)
|
|
|
|
|
|
class DownloadDispatcher:
|
|
def __init__(self, middlewares=AUTO_CONFIGURE):
|
|
#: List of children middlewares.
|
|
self.middlewares = middlewares
|
|
if self.middlewares is AUTO_CONFIGURE:
|
|
self.auto_configure_middlewares()
|
|
|
|
def auto_configure_middlewares(self):
|
|
"""Populate :attr:`middlewares` from
|
|
``settings.DOWNLOADVIEW_MIDDLEWARES``."""
|
|
for key, import_string, kwargs in getattr(
|
|
settings, "DOWNLOADVIEW_MIDDLEWARES", []
|
|
):
|
|
factory = import_member(import_string)
|
|
middleware = factory(**kwargs)
|
|
self.middlewares.append((key, middleware))
|
|
|
|
def dispatch(self, request, response):
|
|
"""Dispatches job to children middlewares."""
|
|
for key, middleware in self.middlewares:
|
|
response = middleware.process_response(request, response)
|
|
return response
|
|
|
|
|
|
class DownloadDispatcherMiddleware(BaseDownloadMiddleware):
|
|
"Download middleware that dispatches job to several middleware instances."
|
|
|
|
def __init__(self, get_response, middlewares=AUTO_CONFIGURE):
|
|
super().__init__(get_response)
|
|
self.dispatcher = DownloadDispatcher(middlewares)
|
|
|
|
def process_download_response(self, request, response):
|
|
return self.dispatcher.dispatch(request, response)
|
|
|
|
|
|
class SmartDownloadMiddleware(DownloadDispatcherMiddleware):
|
|
"""Easy to configure download middleware."""
|
|
|
|
def __init__(
|
|
self,
|
|
get_response,
|
|
backend_factory=AUTO_CONFIGURE,
|
|
backend_options=AUTO_CONFIGURE,
|
|
):
|
|
"""Constructor."""
|
|
super().__init__(get_response, middlewares=[])
|
|
#: Callable (typically a class) to instantiate 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 instantiate 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 = copy.deepcopy(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.abc.Mapping): # Using kwargs.
|
|
kwargs = options
|
|
else:
|
|
args = options
|
|
if "backend" in kwargs: # Specific backend for this rule.
|
|
factory = import_member(kwargs["backend"])
|
|
del kwargs["backend"]
|
|
else: # Fallback to global backend.
|
|
factory = self.backend_factory
|
|
middleware_instance = factory(*args, **kwargs)
|
|
self.dispatcher.middlewares.append((key, middleware_instance))
|
|
|
|
|
|
class NoRedirectionMatch(Exception):
|
|
"""Response object does not match redirection rules."""
|
|
|
|
|
|
class ProxiedDownloadMiddleware(RealDownloadMiddleware):
|
|
"""Base class for middlewares that use optimizations of reverse proxies."""
|
|
|
|
def __init__(
|
|
self, get_response, source_dir=None, source_url=None, destination_url=None
|
|
):
|
|
"""Constructor."""
|
|
super().__init__(get_response)
|
|
|
|
self.source_dir = source_dir
|
|
self.source_url = source_url
|
|
self.destination_url = destination_url
|
|
|
|
def get_redirect_url(self, response):
|
|
"""Return redirect URL for file wrapped into response."""
|
|
url = None
|
|
file_url = ""
|
|
if self.source_url:
|
|
try:
|
|
file_url = response.file.url
|
|
except AttributeError:
|
|
pass
|
|
else:
|
|
if file_url.startswith(self.source_url):
|
|
file_url = file_url[len(self.source_url) :]
|
|
url = file_url
|
|
file_name = ""
|
|
if url is None and self.source_dir:
|
|
try:
|
|
file_name = response.file.name
|
|
except AttributeError:
|
|
pass
|
|
else:
|
|
if file_name.startswith(self.source_dir):
|
|
file_name = os.path.relpath(file_name, self.source_dir)
|
|
url = file_name.replace(os.path.sep, "/")
|
|
if url is None:
|
|
message = (
|
|
"""Couldn't capture/convert file attributes into a """
|
|
"""redirection. """
|
|
"""``source_url`` is "%(source_url)s", """
|
|
"""file's URL is "%(file_url)s". """
|
|
"""``source_dir`` is "%(source_dir)s", """
|
|
"""file's name is "%(file_name)s". """
|
|
% {
|
|
"source_url": self.source_url,
|
|
"file_url": file_url,
|
|
"source_dir": self.source_dir,
|
|
"file_name": file_name,
|
|
}
|
|
)
|
|
raise NoRedirectionMatch(message)
|
|
return "/".join((self.destination_url.rstrip("/"), url.lstrip("/")))
|