mirror of
https://github.com/jazzband/django-downloadview.git
synced 2026-03-16 22:40:25 +00:00
Refs #41 - Added 'mimetype' and 'encoding' arguments to 'DownloadMixin' => supported all arguments of original sendfile() function. Added documentation about migrating from django-sendfile to django-downloadview.
This commit is contained in:
parent
59d9b4966e
commit
6dd090757a
10 changed files with 197 additions and 28 deletions
|
|
@ -11,8 +11,13 @@ future releases, check `milestones`_ and :doc:`/about/vision`.
|
|||
X-Sendfile support.
|
||||
|
||||
- Feature #2 - Introduced support of Lighttpd's x-Sendfile.
|
||||
|
||||
- Feature #36 - Introduced support of Apache's mod_xsendfile.
|
||||
|
||||
- Feature #41 - ``django_downloadview.sendfile`` is a port of
|
||||
`django-sendfile`'s ``sendfile`` function. The documentation contains notes
|
||||
about migrating from `django-sendfile` to `django-downloadview`.
|
||||
|
||||
|
||||
1.4 (2013-11-24)
|
||||
----------------
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ from django_downloadview.views import (PathDownloadView, # NoQA
|
|||
VirtualDownloadView,
|
||||
BaseDownloadView,
|
||||
DownloadMixin)
|
||||
from django_downloadview.sendfile import sendfile # NoQA
|
||||
from django_downloadview.shortcuts import sendfile # NoQA
|
||||
from django_downloadview.test import (assert_download_response, # NoQA
|
||||
setup_view,
|
||||
temporary_media_root)
|
||||
|
|
|
|||
|
|
@ -57,7 +57,25 @@ def encode_basename_utf8(value):
|
|||
|
||||
|
||||
def content_disposition(filename):
|
||||
"""Return value of ``Content-Disposition`` header."""
|
||||
"""Return value of ``Content-Disposition`` header with 'attachment'.
|
||||
|
||||
>>> content_disposition('demo.txt')
|
||||
'attachment; filename=demo.txt'
|
||||
|
||||
If filename is empty, only "attachment" is returned.
|
||||
|
||||
>>> content_disposition('')
|
||||
'attachment'
|
||||
|
||||
If filename contains non US-ASCII characters, the returned value contains
|
||||
UTF-8 encoded filename and US-ASCII fallback.
|
||||
|
||||
>>> content_disposition(unicode('é.txt', 'utf-8'))
|
||||
"attachment; filename=e.txt; filename*=UTF-8''%C3%A9.txt"
|
||||
|
||||
"""
|
||||
if not filename:
|
||||
return 'attachment'
|
||||
ascii_filename = encode_basename_ascii(filename)
|
||||
utf8_filename = encode_basename_utf8(filename)
|
||||
if ascii_filename == utf8_filename: # ASCII only.
|
||||
|
|
@ -99,6 +117,15 @@ class DownloadResponse(StreamingHttpResponse):
|
|||
response (default implementation uses mimetypes, based on file
|
||||
name).
|
||||
|
||||
``file_mimetype``
|
||||
Value for file's mimetype. If ``None`` (the default), then the file's
|
||||
mimetype will be guessed via Python's :mod:`mimetypes`. See
|
||||
:meth:`get_mime_type`.
|
||||
|
||||
``file_encoding``
|
||||
Value for file's encoding. If ``None`` (the default), then the file's
|
||||
encoding will be guessed via Python's :mod:`mimetypes`. See
|
||||
:meth:`get_encoding`.
|
||||
|
||||
Here are some highlights to understand internal mechanisms and motivations:
|
||||
|
||||
|
|
@ -125,7 +152,8 @@ class DownloadResponse(StreamingHttpResponse):
|
|||
|
||||
"""
|
||||
def __init__(self, file_instance, attachment=True, basename=None,
|
||||
status=200, content_type=None):
|
||||
status=200, content_type=None, file_mimetype=None,
|
||||
file_encoding=None):
|
||||
"""Constructor."""
|
||||
self.file = file_instance
|
||||
super(DownloadResponse, self).__init__(streaming_content=self.file,
|
||||
|
|
@ -135,6 +163,8 @@ class DownloadResponse(StreamingHttpResponse):
|
|||
self.attachment = attachment
|
||||
if not content_type:
|
||||
del self['Content-Type'] # Will be set later.
|
||||
self.file_mimetype = file_mimetype
|
||||
self.file_encoding = file_encoding
|
||||
# Apply default headers.
|
||||
for header, value in self.default_headers.items():
|
||||
if not header in self:
|
||||
|
|
@ -195,6 +225,8 @@ class DownloadResponse(StreamingHttpResponse):
|
|||
|
||||
def get_mime_type(self):
|
||||
"""Return mime-type of the file."""
|
||||
if self.file_mimetype is not None:
|
||||
return self.file_mimetype
|
||||
default_mime_type = 'application/octet-stream'
|
||||
basename = self.get_basename()
|
||||
mime_type, encoding = mimetypes.guess_type(basename)
|
||||
|
|
@ -202,6 +234,8 @@ class DownloadResponse(StreamingHttpResponse):
|
|||
|
||||
def get_encoding(self):
|
||||
"""Return encoding of the file to serve."""
|
||||
if self.file_encoding is not None:
|
||||
return self.file_encoding
|
||||
basename = self.get_basename()
|
||||
mime_type, encoding = mimetypes.guess_type(basename)
|
||||
return encoding
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Port of django-sendfile in django-downloadview."""
|
||||
from django_downloadview.views.path import PathDownloadView
|
||||
|
||||
|
||||
def sendfile(request, filename, attachment=False, attachment_filename=None,
|
||||
mimetype=None, encoding=None):
|
||||
"""Port of django-sendfile's API in django-downloadview.
|
||||
|
||||
Instantiates a :class:`~django.core.files.storage.FileSystemStorage` with
|
||||
``settings.SENDFILE_ROOT`` as root folder. Then uses
|
||||
:class:`StorageDownloadView` to stream the file by ``filename``.
|
||||
|
||||
"""
|
||||
view = PathDownloadView().as_view(path=filename,
|
||||
attachment=attachment,
|
||||
basename=attachment_filename)
|
||||
return view(request)
|
||||
19
django_downloadview/shortcuts.py
Normal file
19
django_downloadview/shortcuts.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Port of django-sendfile in django-downloadview."""
|
||||
from django_downloadview.views.path import PathDownloadView
|
||||
|
||||
|
||||
def sendfile(request, filename, attachment=False, attachment_filename=None,
|
||||
mimetype=None, encoding=None):
|
||||
"""Port of django-sendfile's API in django-downloadview.
|
||||
|
||||
Instantiates a :class:`~django_downloadview.views.path.PathDownloadView` to
|
||||
stream the file by ``filename``.
|
||||
|
||||
"""
|
||||
view = PathDownloadView.as_view(path=filename,
|
||||
attachment=attachment,
|
||||
basename=attachment_filename,
|
||||
mimetype=mimetype,
|
||||
encoding=encoding)
|
||||
return view(request)
|
||||
|
|
@ -14,7 +14,9 @@ import django.test
|
|||
|
||||
from django_downloadview import exceptions
|
||||
from django_downloadview.test import setup_view
|
||||
from django_downloadview.response import DownloadResponse
|
||||
from django_downloadview import views
|
||||
from django_downloadview.shortcuts import sendfile
|
||||
|
||||
|
||||
class DownloadMixinTestCase(unittest.TestCase):
|
||||
|
|
@ -121,7 +123,9 @@ class DownloadMixinTestCase(unittest.TestCase):
|
|||
response_kwargs = {'dummy': 'value',
|
||||
'file_instance': mock.sentinel.file_wrapper,
|
||||
'attachment': True,
|
||||
'basename': None}
|
||||
'basename': None,
|
||||
'file_mimetype': None,
|
||||
'file_encoding': None}
|
||||
response = mixin.download_response(**response_kwargs)
|
||||
self.assertIs(response, mock.sentinel.response)
|
||||
response_factory.assert_called_once_with(**response_kwargs) # Not args
|
||||
|
|
@ -261,3 +265,38 @@ class ObjectDownloadViewTestCase(unittest.TestCase):
|
|||
view.object.other_field = None
|
||||
with self.assertRaises(exceptions.FileNotFound):
|
||||
view.get_file()
|
||||
|
||||
|
||||
class SendfileTestCase(django.test.TestCase):
|
||||
"""Tests around :func:`django_downloadview.sendfile.sendfile`."""
|
||||
def test_defaults(self):
|
||||
"""sendfile() takes at least request and filename."""
|
||||
request = django.test.RequestFactory().get('/fake-url')
|
||||
filename = __file__
|
||||
response = sendfile(request, filename)
|
||||
self.assertTrue(isinstance(response, DownloadResponse))
|
||||
self.assertFalse(response.attachment)
|
||||
|
||||
def test_custom(self):
|
||||
"""sendfile() accepts various arguments for response tuning."""
|
||||
request = django.test.RequestFactory().get('/fake-url')
|
||||
filename = __file__
|
||||
response = sendfile(request,
|
||||
filename,
|
||||
attachment=True,
|
||||
attachment_filename='toto.txt',
|
||||
mimetype='test/octet-stream',
|
||||
encoding='gzip')
|
||||
self.assertTrue(isinstance(response, DownloadResponse))
|
||||
self.assertTrue(response.attachment)
|
||||
self.assertEqual(response.basename, 'toto.txt')
|
||||
self.assertEqual(response['Content-Type'],
|
||||
'test/octet-stream; charset=utf-8')
|
||||
self.assertEqual(response.get_encoding(), 'gzip')
|
||||
|
||||
def test_404(self):
|
||||
"""sendfile() raises Http404 if file does not exists."""
|
||||
request = django.test.RequestFactory().get('/fake-url')
|
||||
filename = 'i-do-no-exist'
|
||||
with self.assertRaises(Http404):
|
||||
sendfile(request, filename)
|
||||
|
|
|
|||
|
|
@ -33,6 +33,16 @@ class DownloadMixin(object):
|
|||
#: 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.
|
||||
|
||||
|
|
@ -43,8 +53,29 @@ class DownloadMixin(object):
|
|||
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``.
|
||||
|
||||
|
|
@ -81,6 +112,8 @@ class DownloadMixin(object):
|
|||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,11 @@ django-sendfile
|
|||
`django-sendfile`_ is a wrapper around web-server specific methods for sending
|
||||
files to web clients.
|
||||
|
||||
.. note::
|
||||
|
||||
:func:`django_downloadview.shortcuts.sendfile` is a port of
|
||||
`django-sendfile`'s main function. See :doc:`/django-sendfile` for details.
|
||||
|
||||
``django-senfile``'s main focus is simplicity: API is made of a single
|
||||
``sendfile()`` function you call inside your views:
|
||||
|
||||
|
|
@ -64,12 +69,6 @@ Here are main differences between the two projects:
|
|||
root folder. Whereas ``django-downloadview``'s
|
||||
``DownloadDispatcherMiddleware`` supports multiple configurations.
|
||||
|
||||
As of 2012-04-11, ``django-sendfile`` (version 0.3.2) seems quite popular and
|
||||
may be a good alternative **provided you serve files that live in a single
|
||||
directory of local filesystem**.
|
||||
|
||||
:func:`django_downloadview.sendfile` is a port of django-sendfile's main function.
|
||||
|
||||
|
||||
.. rubric:: References
|
||||
|
||||
|
|
|
|||
57
docs/django-sendfile.txt
Normal file
57
docs/django-sendfile.txt
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
##############################
|
||||
Migrating from django-sendfile
|
||||
##############################
|
||||
|
||||
`django-sendfile`_ is a wrapper around web-server specific methods for sending
|
||||
files to web clients. See :doc:`/about/alternatives` for details about this
|
||||
project.
|
||||
|
||||
`django-downloadview` provides a :func:`port of django-sendfile's main function
|
||||
<django_downloadview.shortcuts.sendfile>`.
|
||||
|
||||
.. warning::
|
||||
|
||||
`django-downloadview` can replace the following `django-sendfile`'s
|
||||
backends: ``nginx``, ``xsendfile``, ``simple``. But it currently cannot
|
||||
replace ``mod_wsgi`` backend.
|
||||
|
||||
Here are tips to migrate from `django-sendfile` to `django-downloadview`...
|
||||
|
||||
1. In your project's and apps dependencies, replace ``django-sendfile`` by
|
||||
``django-downloadview``.
|
||||
|
||||
2. In your Python scripts, replace ``import sendfile`` and ``from sendfile``
|
||||
by ``import django_downloadview`` and ``from django_downloadview``.
|
||||
You get something like ``from django_downloadview import sendfile``
|
||||
|
||||
3. Adapt your settings as explained in :doc:`/settings`. Pay attention to:
|
||||
|
||||
* replace ``sendfile`` by ``django_downloadview`` in ``INSTALLED_APPS``.
|
||||
* replace ``SENDFILE_BACKEND`` by ``DOWNLOADVIEW_BACKEND``
|
||||
* setup ``DOWNLOADVIEW_RULES``. It replaces ``SENDFILE_ROOT`` and can do
|
||||
more.
|
||||
* register ``django_downloadview.SmartDownloadMiddleware`` in
|
||||
``MIDDLEWARE_CLASSES``.
|
||||
|
||||
4. Change your tests if any. You can no longer use `django-senfile`'s
|
||||
``development`` backend. See :doc:`/testing` for `django-downloadview`'s
|
||||
toolkit.
|
||||
|
||||
5. Here you are! ... or please report your story/bug at `django-downloadview's
|
||||
bugtracker`_ ;)
|
||||
|
||||
|
||||
*************
|
||||
API reference
|
||||
*************
|
||||
|
||||
.. autofunction:: django_downloadview.shortcuts.sendfile
|
||||
|
||||
|
||||
.. rubric:: References
|
||||
|
||||
.. target-notes::
|
||||
|
||||
.. _`django-sendfile`: http://pypi.python.org/pypi/django-sendfile
|
||||
.. _`django-downloadview's bugtracker`:
|
||||
https://github.com/benoitbryon/django-downloadview/issues
|
||||
|
|
@ -18,6 +18,7 @@ Contents
|
|||
healthchecks
|
||||
files
|
||||
responses
|
||||
django-sendfile
|
||||
demo
|
||||
about/index
|
||||
dev
|
||||
|
|
|
|||
Loading…
Reference in a new issue