mirror of
https://github.com/jazzband/django-downloadview.git
synced 2026-03-16 22:40:25 +00:00
Refs #21 and refs #23 - Download view passes a file wrapper to download response. The file wrapper encapsulates file attributes such as name, size or URL (introduced URL).
This commit is contained in:
parent
dd687d148b
commit
a012b11e97
6 changed files with 476 additions and 278 deletions
133
django_downloadview/files.py
Normal file
133
django_downloadview/files.py
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
"""File wrappers for use as exchange data between views and responses."""
|
||||
from django.core.files import File
|
||||
|
||||
|
||||
class StorageFile(File):
|
||||
"""A file in a Django storage.
|
||||
|
||||
This class looks like :py:class:`django.db.models.fields.files.FieldFile`,
|
||||
but unrelated to model instance.
|
||||
|
||||
"""
|
||||
def __init__(self, storage, name, file=None):
|
||||
"""Constructor.
|
||||
|
||||
storage:
|
||||
Some :py:class:`django.core.files.storage.Storage` instance.
|
||||
|
||||
name:
|
||||
File identifier in storage, usually a filename as a string.
|
||||
|
||||
"""
|
||||
self.storage = storage
|
||||
self.name = name
|
||||
self.file = file
|
||||
|
||||
def _get_file(self):
|
||||
"""Getter for :py:attr:``file`` property."""
|
||||
if not hasattr(self, '_file') or self._file is None:
|
||||
self._file = self.storage.open(self.name, 'rb')
|
||||
return self._file
|
||||
|
||||
def _set_file(self, file):
|
||||
"""Setter for :py:attr:``file`` property."""
|
||||
self._file = file
|
||||
|
||||
def _del_file(self):
|
||||
"""Deleter for :py:attr:``file`` property."""
|
||||
del self._file
|
||||
|
||||
#: Required by django.core.files.utils.FileProxy.
|
||||
file = property(_get_file, _set_file, _del_file)
|
||||
|
||||
def open(self, mode='rb'):
|
||||
"""Retrieves the specified file from storage and return open() result.
|
||||
|
||||
Proxy to self.storage.open(self.name, mode).
|
||||
|
||||
"""
|
||||
return self.storage.open(self.name, mode)
|
||||
|
||||
def save(self, content):
|
||||
"""Saves new content to the file.
|
||||
|
||||
Proxy to self.storage.save(self.name).
|
||||
|
||||
The content should be a proper File object, ready to be read from the
|
||||
beginning.
|
||||
|
||||
"""
|
||||
return self.storage.save(self.name, content)
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
"""Return a local filesystem path which is suitable for open().
|
||||
|
||||
Proxy to self.storage.path(self.name).
|
||||
|
||||
May raise NotImplementedError if storage doesn't support file access
|
||||
with Python's built-in open() function
|
||||
|
||||
"""
|
||||
return self.storage.path(self.name)
|
||||
|
||||
def delete(self):
|
||||
"""Delete the specified file from the storage system.
|
||||
|
||||
Proxy to self.storage.delete(self.name).
|
||||
|
||||
"""
|
||||
return self.storage.delete(self.name)
|
||||
|
||||
def exists(self):
|
||||
"""Return True if file already exists in the storage system.
|
||||
|
||||
If False, then the name is available for a new file.
|
||||
|
||||
"""
|
||||
return self.storage.exists(self.name)
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
"""Return the total size, in bytes, of the file.
|
||||
|
||||
Proxy to self.storage.size(self.name).
|
||||
|
||||
"""
|
||||
return self.storage.size(self.name)
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
"""Return an absolute URL where the file's contents can be accessed.
|
||||
|
||||
Proxy to self.storage.url(self.name).
|
||||
|
||||
"""
|
||||
return self.storage.url(self.name)
|
||||
|
||||
@property
|
||||
def accessed_time(self):
|
||||
"""Return the last accessed time (as datetime object) of the file.
|
||||
|
||||
Proxy to self.storage.accessed_time(self.name).
|
||||
|
||||
"""
|
||||
return self.storage.accessed(self.name)
|
||||
|
||||
@property
|
||||
def created_time(self):
|
||||
"""Return the creation time (as datetime object) of the file.
|
||||
|
||||
Proxy to self.storage.created_time(self.name).
|
||||
|
||||
"""
|
||||
return self.storage.created_time(self.name)
|
||||
|
||||
@property
|
||||
def modified_time(self):
|
||||
"""Return the last modification time (as datetime object) of the file.
|
||||
|
||||
Proxy to self.storage.modified_time(self.name).
|
||||
|
||||
"""
|
||||
return self.storage.modified_time(self.name)
|
||||
|
|
@ -19,8 +19,7 @@ class BaseDownloadMiddleware(object):
|
|||
return is_download_response(response)
|
||||
|
||||
def process_response(self, request, response):
|
||||
"""Call :py:meth:`process_download_response` if ``response`` is
|
||||
download."""
|
||||
"""Call :py:meth:`process_download_response` if ``response`` is download."""
|
||||
if self.is_download_response(response):
|
||||
return self.process_download_response(request, response)
|
||||
return response
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ See also `Nginx X-accel documentation <http://wiki.nginx.org/X-accel>`_ and
|
|||
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
import os
|
||||
import warnings
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
|
@ -24,7 +26,7 @@ from django_downloadview.utils import content_type_to_charset
|
|||
#:
|
||||
#: Default value is None, which means "let Nginx choose", i.e. use Nginx
|
||||
#: defaults or specific configuration.
|
||||
#:
|
||||
#:
|
||||
#: If set to ``False``, Nginx buffering is disabled.
|
||||
#: If set to ``True``, Nginx buffering is enabled.
|
||||
DEFAULT_WITH_BUFFERING = None
|
||||
|
|
@ -40,12 +42,13 @@ if not hasattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_WITH_BUFFERING'):
|
|||
#:
|
||||
#: Default value is None, which means "let Nginx choose", i.e. use Nginx
|
||||
#: defaults or specific configuration.
|
||||
#:
|
||||
#:
|
||||
#: If set to ``False``, Nginx limit rate is disabled.
|
||||
#: Else, it indicates the limit rate in bytes.
|
||||
DEFAULT_LIMIT_RATE = None
|
||||
if not hasattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_LIMIT_RATE'):
|
||||
setattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_LIMIT_RATE', DEFAULT_LIMIT_RATE)
|
||||
setattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_LIMIT_RATE',
|
||||
DEFAULT_LIMIT_RATE)
|
||||
|
||||
|
||||
#: Default value for X-Accel-Limit-Expires header.
|
||||
|
|
@ -55,7 +58,7 @@ if not hasattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_LIMIT_RATE'):
|
|||
#:
|
||||
#: Default value is None, which means "let Nginx choose", i.e. use Nginx
|
||||
#: defaults or specific configuration.
|
||||
#:
|
||||
#:
|
||||
#: If set to ``False``, Nginx buffering is disabled.
|
||||
#: Else, it indicates the expiration delay, in seconds.
|
||||
DEFAULT_EXPIRES = None
|
||||
|
|
@ -63,6 +66,40 @@ if not hasattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_EXPIRES'):
|
|||
setattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_EXPIRES', DEFAULT_EXPIRES)
|
||||
|
||||
|
||||
#: Default value for settings.NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR.
|
||||
DEFAULT_SOURCE_DIR = settings.MEDIA_ROOT
|
||||
if hasattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT'):
|
||||
warnings.warn("settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT is "
|
||||
"deprecated, use "
|
||||
"settings.NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR instead.",
|
||||
DeprecationWarning)
|
||||
DEFAULT_SOURCE_DIR = settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT
|
||||
if not hasattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR'):
|
||||
setattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR',
|
||||
DEFAULT_SOURCE_DIR)
|
||||
|
||||
|
||||
#: Default value for settings.NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_URL.
|
||||
DEFAULT_SOURCE_URL = settings.MEDIA_URL
|
||||
if not hasattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_URL'):
|
||||
setattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_URL',
|
||||
DEFAULT_SOURCE_URL)
|
||||
|
||||
|
||||
#: Default value for settings.NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL.
|
||||
DEFAULT_DESTINATION_URL = None
|
||||
if hasattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL'):
|
||||
warnings.warn("settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL is "
|
||||
"deprecated, use "
|
||||
"settings.NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL "
|
||||
"instead.",
|
||||
DeprecationWarning)
|
||||
DEFAULT_SOURCE_DIR = settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL
|
||||
if not hasattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL'):
|
||||
setattr(settings, 'NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL',
|
||||
DEFAULT_DESTINATION_URL)
|
||||
|
||||
|
||||
class XAccelRedirectResponse(HttpResponse):
|
||||
"""Http response that delegates serving file to Nginx."""
|
||||
def __init__(self, redirect_url, content_type, basename=None, expires=None,
|
||||
|
|
@ -81,8 +118,9 @@ class XAccelRedirectResponse(HttpResponse):
|
|||
elif expires is not None: # We explicitely want it off.
|
||||
self['X-Accel-Expires'] = 'off'
|
||||
if limit_rate is not None:
|
||||
self['X-Accel-Limit-Rate'] = limit_rate and '%d' % limit_rate \
|
||||
or 'off'
|
||||
self['X-Accel-Limit-Rate'] = (limit_rate
|
||||
and '%d' % limit_rate
|
||||
or 'off')
|
||||
|
||||
|
||||
class XAccelRedirectValidator(object):
|
||||
|
|
@ -95,33 +133,33 @@ class XAccelRedirectValidator(object):
|
|||
"""Assert that ``response`` is a valid X-Accel-Redirect response.
|
||||
|
||||
Optional ``assertions`` dictionary can be used to check additional
|
||||
items:
|
||||
|
||||
items:
|
||||
|
||||
* ``basename``: the basename of the file in the response.
|
||||
|
||||
|
||||
* ``content_type``: the value of "Content-Type" header.
|
||||
|
||||
|
||||
* ``redirect_url``: the value of "X-Accel-Redirect" header.
|
||||
|
||||
|
||||
* ``charset``: the value of ``X-Accel-Charset`` header.
|
||||
|
||||
|
||||
* ``with_buffering``: the value of ``X-Accel-Buffering`` header.
|
||||
If ``False``, then makes sure that the header disables buffering.
|
||||
If ``None``, then makes sure that the header is not set.
|
||||
|
||||
|
||||
* ``expires``: the value of ``X-Accel-Expires`` header.
|
||||
If ``False``, then makes sure that the header disables expiration.
|
||||
If ``None``, then makes sure that the header is not set.
|
||||
|
||||
|
||||
* ``limit_rate``: the value of ``X-Accel-Limit-Rate`` header.
|
||||
If ``False``, then makes sure that the header disables limit rate.
|
||||
If ``None``, then makes sure that the header is not set.
|
||||
|
||||
"""
|
||||
self.assert_x_accel_redirect_response(test_case, response)
|
||||
self.assert_x_accel_redirect_response(test_case, response)
|
||||
for key, value in assertions.iteritems():
|
||||
assert_func = getattr(self, 'assert_%s' % key)
|
||||
assert_func(test_case, response, value)
|
||||
assert_func(test_case, response, value)
|
||||
|
||||
def assert_x_accel_redirect_response(self, test_case, response):
|
||||
test_case.assertTrue(isinstance(response, XAccelRedirectResponse))
|
||||
|
|
@ -205,21 +243,92 @@ class BaseXAccelRedirectMiddleware(BaseDownloadMiddleware):
|
|||
:py:class:`django_downloadview.decorators.DownloadDecorator`.
|
||||
|
||||
"""
|
||||
def __init__(self, media_root, media_url, expires=None,
|
||||
with_buffering=None, limit_rate=None):
|
||||
def __init__(self, source_dir=None, source_url=None, destination_url=None,
|
||||
expires=None, with_buffering=None, limit_rate=None,
|
||||
media_root=None, media_url=None):
|
||||
"""Constructor."""
|
||||
self.media_root = media_root
|
||||
self.media_url = media_url
|
||||
if media_url is not None:
|
||||
warnings.warn("%s ``media_url`` is deprecated. Use "
|
||||
"``destination_url`` instead."
|
||||
% self.__class__.__name__,
|
||||
DeprecationWarning)
|
||||
if destination_url is None:
|
||||
self.destination_url = media_url
|
||||
else:
|
||||
self.destination_url = destination_url
|
||||
else:
|
||||
self.destination_url = destination_url
|
||||
if media_root is not None:
|
||||
warnings.warn("%s ``media_root`` is deprecated. Use "
|
||||
"``source_dir`` instead." % self.__class__.__name__,
|
||||
DeprecationWarning)
|
||||
if source_dir is None:
|
||||
self.source_dir = media_root
|
||||
else:
|
||||
self.source_dir = source_dir
|
||||
else:
|
||||
self.source_dir = source_dir
|
||||
self.source_url = source_url
|
||||
self.expires = expires
|
||||
self.with_buffering = with_buffering
|
||||
self.limit_rate = limit_rate
|
||||
|
||||
def is_download_response(self, response):
|
||||
"""Return True for DownloadResponse, except for "virtual" files.
|
||||
|
||||
This implementation can't 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.
|
||||
|
||||
"""
|
||||
if super(BaseXAccelRedirectMiddleware,
|
||||
self).is_download_response(response):
|
||||
try:
|
||||
response.file.url
|
||||
except AttributeError:
|
||||
try:
|
||||
response.file.name
|
||||
except AttributeError:
|
||||
return False
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_redirect_url(self, response):
|
||||
"""Return redirect URL for file wrapped into response."""
|
||||
absolute_filename = response.filename
|
||||
relative_filename = absolute_filename[len(self.media_root):]
|
||||
return '/'.join((self.media_url.rstrip('/'),
|
||||
relative_filename.strip('/')))
|
||||
url = None
|
||||
file_url = ''
|
||||
if self.source_url is not None:
|
||||
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 is not None:
|
||||
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 ImproperlyConfigured(message)
|
||||
return '/'.join((self.destination_url.rstrip('/'), url.lstrip('/')))
|
||||
|
||||
def process_download_response(self, request, response):
|
||||
"""Replace DownloadResponse instances by NginxDownloadResponse ones."""
|
||||
|
|
@ -240,24 +349,49 @@ class BaseXAccelRedirectMiddleware(BaseDownloadMiddleware):
|
|||
|
||||
|
||||
class XAccelRedirectMiddleware(BaseXAccelRedirectMiddleware):
|
||||
"""Apply X-Accel-Redirect globally, via Django settings."""
|
||||
"""Apply X-Accel-Redirect globally, via Django settings.
|
||||
|
||||
Available settings are:
|
||||
|
||||
NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_URL:
|
||||
The string at the beginning of URLs to replace with
|
||||
``NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL``.
|
||||
If ``None``, then URLs aren't captured.
|
||||
Defaults to ``settings.MEDIA_URL``.
|
||||
|
||||
NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR:
|
||||
The string at the beginning of filenames (path) to replace with
|
||||
``NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL``.
|
||||
If ``None``, then filenames aren't captured.
|
||||
Defaults to ``settings.MEDIA_ROOT``.
|
||||
|
||||
NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL:
|
||||
The base URL where requests are proxied to.
|
||||
If ``None`` an ImproperlyConfigured exception is raised.
|
||||
|
||||
.. note::
|
||||
|
||||
The following settings are deprecated since version 1.1.
|
||||
URLs can be used as redirection source since 1.1, and then "MEDIA_ROOT"
|
||||
and "MEDIA_URL" became too confuse.
|
||||
|
||||
NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT:
|
||||
Replaced by ``NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR``.
|
||||
|
||||
NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL:
|
||||
Replaced by ``NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL``.
|
||||
|
||||
"""
|
||||
def __init__(self):
|
||||
"""Use Django settings as configuration."""
|
||||
try:
|
||||
media_root = settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT
|
||||
except AttributeError:
|
||||
if settings.NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL is None:
|
||||
raise ImproperlyConfigured(
|
||||
'settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_ROOT is required by '
|
||||
'%s middleware' % self.__class__.name)
|
||||
try:
|
||||
media_url = settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL
|
||||
except AttributeError:
|
||||
raise ImproperlyConfigured(
|
||||
'settings.NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL is required by '
|
||||
'%s middleware' % self.__class__.name)
|
||||
'settings.NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL is '
|
||||
'required by %s middleware' % self.__class__.__name__)
|
||||
super(XAccelRedirectMiddleware, self).__init__(
|
||||
media_root,
|
||||
media_url,
|
||||
source_dir=settings.NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_DIR,
|
||||
source_url=settings.NGINX_DOWNLOAD_MIDDLEWARE_SOURCE_URL,
|
||||
destination_url=settings.NGINX_DOWNLOAD_MIDDLEWARE_DESTINATION_URL,
|
||||
expires=settings.NGINX_DOWNLOAD_MIDDLEWARE_EXPIRES,
|
||||
with_buffering=settings.NGINX_DOWNLOAD_MIDDLEWARE_WITH_BUFFERING,
|
||||
limit_rate=settings.NGINX_DOWNLOAD_MIDDLEWARE_LIMIT_RATE)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
"""HttpResponse subclasses."""
|
||||
import os
|
||||
import mimetypes
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse
|
||||
|
||||
|
||||
|
|
@ -9,64 +13,111 @@ class DownloadResponse(HttpResponse):
|
|||
this response "lazy".
|
||||
|
||||
"""
|
||||
def __init__(self, content, content_type, content_length, basename,
|
||||
status=200, content_encoding=None, expires=None,
|
||||
filename=None, url=None):
|
||||
def __init__(self, file_instance, attachment=True, basename=None,
|
||||
status=200, content_type=None):
|
||||
"""Constructor.
|
||||
|
||||
It differs a bit from HttpResponse constructor.
|
||||
|
||||
Required arguments:
|
||||
file_instance:
|
||||
A file wrapper object. Could be a FieldFile.
|
||||
|
||||
* ``content`` is supposed to be an iterable that can read the file.
|
||||
Consider :py:class:`wsgiref.util.FileWrapper`` as a good candidate.
|
||||
attachement:
|
||||
Boolean, whether to return the file as attachment or not. Affects
|
||||
"Content-Disposition" header.
|
||||
Defaults to ``True``.
|
||||
|
||||
* ``content_type`` contains mime-type and charset of the file.
|
||||
It is used as "Content-Type" header.
|
||||
basename:
|
||||
Unicode. Only used if ``attachment`` is ``True``. Client-side name
|
||||
of the file to stream. Affects "Content-Disposition" header.
|
||||
Defaults to basename(``file_instance.name``).
|
||||
|
||||
* ``content_length`` is the size, in bytes, of the file.
|
||||
It is used as "Content-Length" header.
|
||||
status:
|
||||
HTTP status code.
|
||||
Defaults to 200.
|
||||
|
||||
* ``basename`` is the client-side name of the file ("save as" name).
|
||||
It is used in "Content-Disposition" header.
|
||||
|
||||
Optional arguments:
|
||||
|
||||
* ``status`` is HTTP status code.
|
||||
|
||||
* ``content_encoding`` is used for "Content-Encoding" header.
|
||||
|
||||
* ``expires`` is a datetime.
|
||||
It is used to set the "Expires" header.
|
||||
|
||||
* ``filename`` is the server-side name of the file.
|
||||
It may be used by decorators or middlewares.
|
||||
|
||||
* ``url`` is the actual URL of the file content.
|
||||
|
||||
* If Django is to serve the file, then ``url`` should be
|
||||
``request.get_full_path()``. This should be the default behaviour
|
||||
when ``url`` is None.
|
||||
|
||||
* If ``url`` is not None and differs from
|
||||
``request.get_full_path()``, then it means that the actual download
|
||||
should be performed at another location. In that case,
|
||||
DownloadResponse doesn't return a redirection, but ``url`` may be
|
||||
caught and used by download middlewares or decorators (Nginx,
|
||||
Lighttpd...).
|
||||
content_type:
|
||||
Value for "Content-Type" header.
|
||||
If ``None``, then mime-type and encoding will be populated by the
|
||||
response (default implementation uses mimetypes, based on file name).
|
||||
Defaults is ``None``.
|
||||
|
||||
"""
|
||||
super(DownloadResponse, self).__init__(content=content, status=status,
|
||||
self.file = file_instance
|
||||
super(DownloadResponse, self).__init__(content=self.file,
|
||||
status=status,
|
||||
content_type=content_type)
|
||||
self.filename = filename
|
||||
self.basename = basename
|
||||
self['Content-Length'] = content_length
|
||||
if content_encoding:
|
||||
self['Content-Encoding'] = content_encoding
|
||||
self.expires = expires
|
||||
if expires:
|
||||
self['Expires'] = expires
|
||||
self['Content-Disposition'] = 'attachment; filename=%s' % basename
|
||||
self.attachment = attachment
|
||||
if not content_type:
|
||||
del self['Content-Type'] # Will be set later.
|
||||
# Apply default headers.
|
||||
for header, value in self.default_headers.items():
|
||||
if not header in self:
|
||||
self[header] = value # Does self support setdefault?
|
||||
|
||||
@property
|
||||
def default_headers(self):
|
||||
"""Return dictionary of automatically-computed headers.
|
||||
|
||||
Uses an internal ``_default_headers`` cache.
|
||||
Default values are computed if only cache hasn't been set.
|
||||
|
||||
"""
|
||||
try:
|
||||
return self._default_headers
|
||||
except AttributeError:
|
||||
headers = {}
|
||||
headers['Content-Type'] = self.get_content_type()
|
||||
headers['Content-Length'] = self.file.size
|
||||
if self.attachment:
|
||||
headers['Content-Disposition'] = 'attachment; filename=%s' \
|
||||
% self.get_basename()
|
||||
self._default_headers = headers
|
||||
return self._default_headers
|
||||
|
||||
def items(self):
|
||||
"""Return iterable of (header, value).
|
||||
|
||||
This method is called by http handlers just before WSGI's
|
||||
start_response() is called... but it is not called by
|
||||
django.test.ClientHandler! :'(
|
||||
|
||||
"""
|
||||
return super(DownloadResponse, self).items()
|
||||
|
||||
def get_basename(self):
|
||||
"""Return basename."""
|
||||
if self.attachment and self.basename:
|
||||
return self.basename
|
||||
else:
|
||||
return os.path.basename(self.file.name)
|
||||
|
||||
def get_content_type(self):
|
||||
"""Return a suitable "Content-Type" header for ``self.file``."""
|
||||
try:
|
||||
return self.file.content_type
|
||||
except AttributeError:
|
||||
content_type_template = '%(mime_type)s; charset=%(charset)s'
|
||||
return content_type_template % {'mime_type': self.get_mime_type(),
|
||||
'charset': self.get_charset()}
|
||||
|
||||
def get_mime_type(self):
|
||||
"""Return mime-type of the file."""
|
||||
default_mime_type = 'application/octet-stream'
|
||||
basename = self.get_basename()
|
||||
mime_type, encoding = mimetypes.guess_type(basename)
|
||||
return mime_type or default_mime_type
|
||||
|
||||
def get_encoding(self):
|
||||
"""Return encoding of the file to serve."""
|
||||
basename = self.get_basename()
|
||||
mime_type, encoding = mimetypes.guess_type(basename)
|
||||
return encoding
|
||||
|
||||
def get_charset(self):
|
||||
"""Return the charset of the file to serve."""
|
||||
return settings.DEFAULT_CHARSET
|
||||
|
||||
|
||||
def is_download_response(response):
|
||||
|
|
|
|||
|
|
@ -1,16 +1,12 @@
|
|||
"""Views."""
|
||||
import mimetypes
|
||||
import os
|
||||
from wsgiref.util import FileWrapper
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files import File
|
||||
from django.core.files.storage import DefaultStorage
|
||||
from django.http import Http404, HttpResponseNotModified
|
||||
from django.http import HttpResponseNotModified
|
||||
from django.views.generic.base import View
|
||||
from django.views.generic.detail import BaseDetailView
|
||||
from django.views.static import was_modified_since
|
||||
|
||||
from django_downloadview.files import StorageFile
|
||||
from django_downloadview.response import DownloadResponse
|
||||
|
||||
|
||||
|
|
@ -31,123 +27,63 @@ class DownloadMixin(object):
|
|||
#: Response class to be used in render_to_response().
|
||||
response_class = DownloadResponse
|
||||
|
||||
#: Whether to return the response as attachment or not.
|
||||
attachment = True
|
||||
|
||||
#: Client-side filename, if only file is returned as attachment.
|
||||
basename = None
|
||||
|
||||
def get_file(self):
|
||||
"""Return a django.core.files.File object, which is to be served."""
|
||||
"""Return a file wrapper instance."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_filename(self):
|
||||
"""Return server-side absolute filename of the file to serve.
|
||||
|
||||
"filename" is used server-side, whereas "basename" is the filename
|
||||
that the client receives for download (i.e. used client side).
|
||||
|
||||
"""
|
||||
file_obj = self.get_file()
|
||||
return file_obj.name
|
||||
|
||||
def get_basename(self):
|
||||
"""Return client-side filename, without path, of the file to be served.
|
||||
return self.basename
|
||||
|
||||
"basename" is the filename that the client receives for download,
|
||||
whereas "filename" is used server-side.
|
||||
|
||||
The base implementation returns the basename of the server-side
|
||||
filename.
|
||||
|
||||
You may override this method to change the behavior.
|
||||
|
||||
"""
|
||||
return os.path.basename(self.get_filename())
|
||||
|
||||
def get_file_wrapper(self):
|
||||
"""Return a wsgiref.util.FileWrapper instance for the file to serve."""
|
||||
try:
|
||||
return self.file_wrapper
|
||||
except AttributeError:
|
||||
self.file_wrapper = FileWrapper(self.get_file())
|
||||
return self.file_wrapper
|
||||
|
||||
def get_mime_type(self):
|
||||
"""Return mime-type of the file to serve."""
|
||||
try:
|
||||
return self.mime_type
|
||||
except AttributeError:
|
||||
basename = self.get_basename()
|
||||
self.mime_type, self.encoding = mimetypes.guess_type(basename)
|
||||
if not self.mime_type:
|
||||
self.mime_type = 'application/octet-stream'
|
||||
return self.mime_type
|
||||
|
||||
def get_encoding(self):
|
||||
"""Return encoding of the file to serve."""
|
||||
try:
|
||||
return self.encoding
|
||||
except AttributeError:
|
||||
filename = self.get_filename()
|
||||
self.mime_type, self.encoding = mimetypes.guess_type(filename)
|
||||
return self.encoding
|
||||
|
||||
def get_charset(self):
|
||||
"""Return the charset of the file to serve."""
|
||||
try:
|
||||
return self.charset
|
||||
except AttributeError:
|
||||
self.charset = settings.DEFAULT_CHARSET
|
||||
return self.charset
|
||||
|
||||
def get_modification_time(self):
|
||||
"""Return last modification time of the file to serve."""
|
||||
try:
|
||||
return self.modification_time
|
||||
except AttributeError:
|
||||
self.stat = os.stat(self.get_filename())
|
||||
self.modification_time = self.stat.st_mtime
|
||||
return self.modification_time
|
||||
|
||||
def get_size(self):
|
||||
"""Return the size (in bytes) of the file to serve."""
|
||||
try:
|
||||
return self.size
|
||||
except AttributeError:
|
||||
try:
|
||||
self.size = self.stat.st_size
|
||||
except AttributeError:
|
||||
self.size = os.path.getsize(self.get_filename())
|
||||
return self.size
|
||||
|
||||
def render_to_response(self, **kwargs):
|
||||
def render_to_response(self, *args, **kwargs):
|
||||
"""Returns a response with a file as attachment."""
|
||||
mime_type = self.get_mime_type()
|
||||
charset = self.get_charset()
|
||||
content_type = '%s; charset=%s' % (mime_type, charset)
|
||||
modification_time = self.get_modification_time()
|
||||
size = self.get_size()
|
||||
# Respect the If-Modified-Since header.
|
||||
file_instance = self.get_file()
|
||||
if_modified_since = self.request.META.get('HTTP_IF_MODIFIED_SINCE',
|
||||
None)
|
||||
if not was_modified_since(if_modified_since, modification_time, size):
|
||||
return HttpResponseNotModified(content_type=content_type)
|
||||
# Stream the file.
|
||||
filename = self.get_filename()
|
||||
basename = self.get_basename()
|
||||
encoding = self.get_encoding()
|
||||
wrapper = self.get_file_wrapper()
|
||||
response_kwargs = {'content': wrapper,
|
||||
'content_type': content_type,
|
||||
'content_length': size,
|
||||
'filename': filename,
|
||||
'basename': basename,
|
||||
'content_encoding': encoding,
|
||||
'expires': None}
|
||||
if if_modified_since is not None:
|
||||
modification_time = file_instance.modified_time
|
||||
size = file_instance.size
|
||||
if not was_modified_since(if_modified_since, modification_time,
|
||||
size):
|
||||
content_type = file_instance.content_type
|
||||
return HttpResponseNotModified(content_type=content_type)
|
||||
# Return download response.
|
||||
response_kwargs = {'file_instance': file_instance,
|
||||
'attachment': self.attachment,
|
||||
'basename': self.get_basename()}
|
||||
response_kwargs.update(kwargs)
|
||||
response = self.response_class(**response_kwargs)
|
||||
# Do not close the file as response class may need it open: the wrapper
|
||||
# is an iterator on the content of the file.
|
||||
# Garbage collector will close the file.
|
||||
return response
|
||||
|
||||
|
||||
class DownloadView(DownloadMixin, View):
|
||||
class BaseDownloadView(DownloadMixin, View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Handle GET requests: stream a file."""
|
||||
return self.render_to_response()
|
||||
|
||||
|
||||
class StorageDownloadView():
|
||||
"""Download a file from storage and filename."""
|
||||
storage = DefaultStorage()
|
||||
path = None
|
||||
|
||||
|
||||
class SimpleDownloadView():
|
||||
"""Download a file from filename."""
|
||||
path = None
|
||||
|
||||
|
||||
class VirtualDownloadView():
|
||||
file_obj = None
|
||||
|
||||
|
||||
class DownloadView(BaseDownloadView):
|
||||
"""Download a file from storage and filename."""
|
||||
#: Server-side name (including path) of the file to serve.
|
||||
#:
|
||||
|
|
@ -169,21 +105,10 @@ class DownloadView(DownloadMixin, View):
|
|||
|
||||
def get_file(self):
|
||||
"""Use filename and storage to return file object to serve."""
|
||||
try:
|
||||
return self._file
|
||||
except AttributeError:
|
||||
try:
|
||||
if self.storage:
|
||||
self._file = self.storage.open(self.filename)
|
||||
else:
|
||||
self._file = File(open(self.filename))
|
||||
return self._file
|
||||
except IOError:
|
||||
raise Http404()
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Handle GET requests: stream a file."""
|
||||
return self.render_to_response()
|
||||
if self.storage:
|
||||
return StorageFile(self.storage, self.filename)
|
||||
else:
|
||||
return File(open(self.filename))
|
||||
|
||||
|
||||
class ObjectDownloadView(DownloadMixin, BaseDetailView):
|
||||
|
|
@ -227,75 +152,23 @@ class ObjectDownloadView(DownloadMixin, BaseDetailView):
|
|||
#: Optional name of the model's attribute which contains the size.
|
||||
size_field = None
|
||||
|
||||
def get_object(self):
|
||||
"""Return model instance, using cache or a get_queryset()."""
|
||||
try:
|
||||
return self._object
|
||||
except AttributeError:
|
||||
self._object = super(ObjectDownloadView, self).get_object()
|
||||
return self._object
|
||||
|
||||
object = property(get_object)
|
||||
|
||||
def get_fieldfile(self):
|
||||
"""Return FieldFile instance (i.e. FileField attribute)."""
|
||||
try:
|
||||
return self.fieldfile
|
||||
except AttributeError:
|
||||
self.fieldfile = getattr(self.object, self.file_field)
|
||||
return self.fieldfile
|
||||
|
||||
def get_file(self):
|
||||
"""Return File instance."""
|
||||
return self.get_fieldfile().file
|
||||
|
||||
def get_filename(self):
|
||||
"""Return absolute filename."""
|
||||
file_obj = self.get_file()
|
||||
return file_obj.name
|
||||
"""Return FieldFile instance."""
|
||||
file_instance = getattr(self.object, self.file_field)
|
||||
for field in ('encoding', 'mime_type', 'charset', 'modification_time',
|
||||
'size'):
|
||||
model_field = getattr(self, '%s_field' % field, False)
|
||||
if model_field:
|
||||
value = getattr(self.object, model_field)
|
||||
setattr(file_instance, field, value)
|
||||
return file_instance
|
||||
|
||||
def get_basename(self):
|
||||
"""Return client-side filename."""
|
||||
if self.basename_field:
|
||||
return getattr(self.object, self.basename_field)
|
||||
else:
|
||||
return super(ObjectDownloadView, self).get_basename()
|
||||
|
||||
def get_mime_type(self):
|
||||
"""Return mime-type."""
|
||||
if self.mime_type_field:
|
||||
return getattr(self.object, self.mime_type_field)
|
||||
else:
|
||||
return super(ObjectDownloadView, self).get_mime_type()
|
||||
|
||||
def get_charset(self):
|
||||
"""Return charset of the file to serve."""
|
||||
if self.charset_field:
|
||||
return getattr(self.object, self.charset_field)
|
||||
else:
|
||||
return super(ObjectDownloadView, self).get_charset()
|
||||
|
||||
def get_encoding(self):
|
||||
"""Return encoding of the file to serve."""
|
||||
if self.encoding_field:
|
||||
return getattr(self.object, self.encoding_field)
|
||||
else:
|
||||
return super(ObjectDownloadView, self).get_encoding()
|
||||
|
||||
def get_modification_time(self):
|
||||
"""Return last modification time of the file to serve."""
|
||||
if self.modification_time_field:
|
||||
return getattr(self.object, self.modification_time_field)
|
||||
else:
|
||||
return super(ObjectDownloadView, self).get_modification_time()
|
||||
|
||||
def get_size(self):
|
||||
"""Return size of the file to serve."""
|
||||
if self.size_field:
|
||||
return getattr(self.object, self.size_field)
|
||||
else:
|
||||
return self.get_fieldfile().size
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Handle GET requests: stream a file."""
|
||||
return self.render_to_response()
|
||||
basename = super(ObjectDownloadView, self).get_basename()
|
||||
if basename is None:
|
||||
field = 'basename'
|
||||
model_field = getattr(self, '%s_field' % field, False)
|
||||
if model_field:
|
||||
basename = getattr(self.object, model_field)
|
||||
return basename
|
||||
|
|
|
|||
|
|
@ -17,6 +17,14 @@ django_downloadview Package
|
|||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
:mod:`files` Module
|
||||
-------------------
|
||||
|
||||
.. automodule:: django_downloadview.files
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
:mod:`middlewares` Module
|
||||
-------------------------
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue