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:
Benoît Bryon 2012-12-13 19:01:50 +01:00
parent dd687d148b
commit a012b11e97
6 changed files with 476 additions and 278 deletions

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

View file

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

View file

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

View file

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

View file

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

View file

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