mirror of
https://github.com/jazzband/django-downloadview.git
synced 2026-03-16 22:40:25 +00:00
173 lines
6 KiB
Python
173 lines
6 KiB
Python
"""Base material for download views: :class:`DownloadMixin` and
|
|
:class:`BaseDownloadView`"""
|
|
|
|
import calendar
|
|
|
|
from django.http import Http404, HttpResponseNotModified
|
|
from django.views.generic.base import View
|
|
from django.views.static import was_modified_since
|
|
|
|
from django_downloadview import exceptions
|
|
from django_downloadview.response import DownloadResponse
|
|
|
|
|
|
class DownloadMixin(object):
|
|
"""Placeholders and base implementation to create file download views.
|
|
|
|
.. note::
|
|
|
|
This class does not inherit from
|
|
:py:class:`django.views.generic.base.View`.
|
|
|
|
The :py:meth:`get_file` method is a placeholder subclasses must implement.
|
|
Base implementation raises ``NotImplementedError``.
|
|
|
|
Other methods provide a base implementation that use the file wrapper
|
|
returned by :py:meth:`get_file`.
|
|
|
|
"""
|
|
|
|
#: Response class, to be used in :py:meth:`render_to_response`.
|
|
response_class = DownloadResponse
|
|
|
|
#: Whether to return the response as attachment or not.
|
|
#:
|
|
#: When ``True`` (the default), the view returns file "as attachment",
|
|
#: which usually triggers a "Save the file as ..." prompt.
|
|
#:
|
|
#: When ``False``, the view returns file "inline", as if it was an element
|
|
#: of the current page.
|
|
#:
|
|
#: .. note::
|
|
#:
|
|
#: The actual behaviour client-side depends on the browser and its
|
|
#: configuration.
|
|
#:
|
|
#: In fact, affects the "Content-Disposition" header via :attr:`response's
|
|
#: attachment attribute
|
|
#: <django_downloadview.response.DownloadResponse.attachment>`.
|
|
attachment = True
|
|
|
|
#: Client-side filename, if only file is returned as attachment.
|
|
basename = None
|
|
|
|
#: File's mime type.
|
|
#: If ``None`` (the default), then the file's mime type will be guessed via
|
|
#: :mod:`mimetypes`.
|
|
mimetype = None
|
|
|
|
#: File's encoding.
|
|
#: If ``None`` (the default), then the file's encoding will be guessed via
|
|
#: :mod:`mimetypes`.
|
|
encoding = None
|
|
|
|
def get_file(self):
|
|
"""Return a file wrapper instance.
|
|
|
|
Raises :class:`~django_downloadview.exceptions.FileNotFound` if file
|
|
does not exist.
|
|
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def get_basename(self):
|
|
"""Return :attr:`basename`.
|
|
|
|
Override this method if you need more dynamic basename.
|
|
|
|
"""
|
|
return self.basename
|
|
|
|
def get_mimetype(self):
|
|
"""Return :attr:`mimetype`.
|
|
|
|
Override this method if you need more dynamic mime type.
|
|
|
|
"""
|
|
return self.mimetype
|
|
|
|
def get_encoding(self):
|
|
"""Return :attr:`encoding`.
|
|
|
|
Override this method if you need more dynamic encoding.
|
|
|
|
"""
|
|
return self.encoding
|
|
|
|
def was_modified_since(self, file_instance, since):
|
|
"""Return True if ``file_instance`` was modified after ``since``.
|
|
|
|
Uses file wrapper's ``was_modified_since`` if available, with value of
|
|
``since`` as positional argument.
|
|
|
|
Else, fallbacks to default implementation, which uses
|
|
:py:func:`django.views.static.was_modified_since`.
|
|
|
|
Django's ``was_modified_since`` function needs a datetime.
|
|
It is passed the ``modified_time`` attribute from file
|
|
wrapper. If file wrapper does not support this attribute
|
|
(``AttributeError`` or ``NotImplementedError`` is raised), then
|
|
the file is considered as modified and ``True`` is returned.
|
|
|
|
"""
|
|
try:
|
|
return file_instance.was_modified_since(since)
|
|
except (AttributeError, NotImplementedError):
|
|
try:
|
|
modification_time = calendar.timegm(
|
|
file_instance.modified_time.utctimetuple()
|
|
)
|
|
except (AttributeError, NotImplementedError) as e:
|
|
print("!=======!", e)
|
|
return True
|
|
else:
|
|
return was_modified_since(since, modification_time)
|
|
|
|
def not_modified_response(self, *response_args, **response_kwargs):
|
|
"""Return :class:`django.http.HttpResponseNotModified` instance."""
|
|
return HttpResponseNotModified(*response_args, **response_kwargs)
|
|
|
|
def download_response(self, *response_args, **response_kwargs):
|
|
"""Return :class:`~django_downloadview.response.DownloadResponse`."""
|
|
response_kwargs.setdefault("file_instance", self.file_instance)
|
|
response_kwargs.setdefault("attachment", self.attachment)
|
|
response_kwargs.setdefault("basename", self.get_basename())
|
|
response_kwargs.setdefault("file_mimetype", self.get_mimetype())
|
|
response_kwargs.setdefault("file_encoding", self.get_encoding())
|
|
response = self.response_class(*response_args, **response_kwargs)
|
|
return response
|
|
|
|
def file_not_found_response(self):
|
|
"""Raise Http404."""
|
|
raise Http404()
|
|
|
|
def render_to_response(self, *response_args, **response_kwargs):
|
|
"""Return "download" response (if everything is ok).
|
|
|
|
Return :meth:`file_not_found_response` if file does not exist.
|
|
|
|
Respects the "HTTP_IF_MODIFIED_SINCE" header if any. In that case, uses
|
|
:py:meth:`was_modified_since` and :py:meth:`not_modified_response`.
|
|
|
|
Else, uses :py:meth:`download_response` to return a download response.
|
|
|
|
"""
|
|
try:
|
|
self.file_instance = self.get_file()
|
|
except exceptions.FileNotFound:
|
|
return self.file_not_found_response()
|
|
# Respect the If-Modified-Since header.
|
|
since = self.request.headers.get("if-modified-since", None)
|
|
if since is not None:
|
|
if not self.was_modified_since(self.file_instance, since):
|
|
return self.not_modified_response(**response_kwargs)
|
|
# Return download response.
|
|
return self.download_response(*response_args, **response_kwargs)
|
|
|
|
|
|
class BaseDownloadView(DownloadMixin, View):
|
|
"""A base :class:`DownloadMixin` that implements :meth:`get`."""
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
"""Handle GET requests: stream a file."""
|
|
return self.render_to_response()
|