mirror of
https://github.com/jazzband/django-downloadview.git
synced 2026-05-19 04:51:13 +00:00
Merge pull request #24 from novagile/23-file-wrapper
Refs #21 and closes #23 - Download view passes a file wrapper to download response.
This commit is contained in:
commit
c671037db0
23 changed files with 857 additions and 370 deletions
1
AUTHORS
1
AUTHORS
|
|
@ -8,3 +8,4 @@ Original code by `Novapost <http://www.novapost.fr>`_ team:
|
|||
* Lauréline Guérin <https://github.com/zebuline>
|
||||
* Gregory Tappero <https://github.com/coulix>
|
||||
* Benoît Bryon <benoit@marmelune.net>
|
||||
* Rémy Hubscher <remy.hubscher@novapost.fr>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,12 @@ Changelog
|
|||
1.1 (unreleased)
|
||||
----------------
|
||||
|
||||
- Nothing changed yet.
|
||||
**Backward incompatible changes.**
|
||||
|
||||
- Download views and response now use file wrappers. Most logic around file
|
||||
attributes, formerly in views, moved to wrappers.
|
||||
- Replaced DownloadView by PathDownloadView and StorageDownloadView. Use the
|
||||
right one depending on the use case.
|
||||
|
||||
|
||||
1.0 (2012-12-04)
|
||||
|
|
|
|||
31
Makefile
31
Makefile
|
|
@ -6,10 +6,19 @@ ROOT_DIR = $(shell pwd)
|
|||
DATA_DIR = $(ROOT_DIR)/var
|
||||
WGET = wget
|
||||
PYTHON = python
|
||||
BUILDOUT_BOOTSTRAP_URL = https://raw.github.com/buildout/buildout/1.6.3/bootstrap/bootstrap.py
|
||||
BUILDOUT_CFG = $(ROOT_DIR)/etc/buildout.cfg
|
||||
BUILDOUT_BOOTSTRAP_URL = https://raw.github.com/buildout/buildout/1.7.0/bootstrap/bootstrap.py
|
||||
BUILDOUT_BOOTSTRAP = $(ROOT_DIR)/lib/buildout/bootstrap.py
|
||||
BUILDOUT_BOOTSTRAP_ARGS = -c $(BUILDOUT_CFG) --distribute buildout:directory=$(ROOT_DIR)
|
||||
BUILDOUT = $(ROOT_DIR)/bin/buildout
|
||||
BUILDOUT_ARGS = -N
|
||||
BUILDOUT_ARGS = -N -c $(BUILDOUT_CFG) buildout:directory=$(ROOT_DIR)
|
||||
|
||||
|
||||
configure:
|
||||
# Configuration is stored in etc/ folder.
|
||||
|
||||
|
||||
develop: buildout
|
||||
|
||||
|
||||
buildout:
|
||||
|
|
@ -20,18 +29,12 @@ buildout:
|
|||
fi
|
||||
# Bootstrap buildout.
|
||||
if [ ! -f $(BUILDOUT) ]; then \
|
||||
$(PYTHON) $(BUILDOUT_BOOTSTRAP) --distribute; \
|
||||
$(PYTHON) $(BUILDOUT_BOOTSTRAP) $(BUILDOUT_BOOTSTRAP_ARGS); \
|
||||
fi
|
||||
# Run zc.buildout.
|
||||
$(BUILDOUT) $(BUILDOUT_ARGS)
|
||||
|
||||
|
||||
develop: buildout
|
||||
|
||||
|
||||
update: develop
|
||||
|
||||
|
||||
clean:
|
||||
find $(ROOT_DIR)/ -name "*.pyc" -delete
|
||||
find $(ROOT_DIR)/ -name ".noseids" -delete
|
||||
|
|
@ -47,8 +50,16 @@ maintainer-clean: distclean
|
|||
rm -rf $(ROOT_DIR)/lib/
|
||||
|
||||
|
||||
test:
|
||||
test: test-demo test-documentation
|
||||
|
||||
|
||||
test-demo:
|
||||
bin/demo test demo
|
||||
mv $(ROOT_DIR)/.coverage $(ROOT_DIR)/var/test/demo.coverage
|
||||
|
||||
|
||||
test-documentation:
|
||||
bin/nosetests -c $(ROOT_DIR)/etc/nose.cfg sphinxcontrib.testbuild.tests
|
||||
|
||||
|
||||
apidoc:
|
||||
|
|
|
|||
13
README
13
README
|
|
@ -1,5 +1,5 @@
|
|||
###################
|
||||
Django-DownloadView
|
||||
django-downloadview
|
||||
###################
|
||||
|
||||
Django-DownloadView provides (class-based) generic download views for Django.
|
||||
|
|
@ -19,6 +19,17 @@ Example, in some urls.py:
|
|||
url_patterns = ('',
|
||||
url('^download/(?P<slug>[A-Za-z0-9_-]+)/$', download, name='download'),
|
||||
)
|
||||
|
||||
Several views are provided to cover frequent use cases:
|
||||
|
||||
* ``ObjectDownloadView`` when you have a model with a file field.
|
||||
* ``StorageDownloadView`` when you manage files in a storage.
|
||||
* ``PathDownloadView`` when you have an absolute filename on local filesystem.
|
||||
* ``VirtualDownloadView`` when you the file is generated on the fly.
|
||||
|
||||
See :doc:`views` for details.
|
||||
|
||||
Then get increased performances with :doc:`optimizations/index`.
|
||||
|
||||
|
||||
**********
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
# coding=utf8
|
||||
"""Test suite for demoproject.download."""
|
||||
from os import listdir
|
||||
from os.path import abspath, dirname, join
|
||||
|
||||
from django.core.files import File
|
||||
from django.core.urlresolvers import reverse_lazy as reverse
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import TestCase
|
||||
|
||||
from django_downloadview.test import temporary_media_root
|
||||
|
|
@ -24,15 +25,8 @@ class DownloadTestCase(TestCase):
|
|||
for f in listdir(fixtures_dir):
|
||||
self.files[f] = abspath(join(fixtures_dir, f))
|
||||
|
||||
|
||||
class DownloadViewTestCase(DownloadTestCase):
|
||||
"""Test generic DownloadView."""
|
||||
def test_download_hello_world(self):
|
||||
"""'download_hello_world' view returns hello-world.txt as attachement.
|
||||
|
||||
"""
|
||||
download_url = reverse('download_hello_world')
|
||||
response = self.client.get(download_url)
|
||||
def assertDownloadHelloWorld(self, response, is_attachment=True):
|
||||
"""Assert response is 'hello-world.txt' download."""
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertEquals(response['Content-Type'],
|
||||
'text/plain; charset=utf-8')
|
||||
|
|
@ -43,23 +37,53 @@ class DownloadViewTestCase(DownloadTestCase):
|
|||
response.content)
|
||||
|
||||
|
||||
class PathDownloadViewTestCase(DownloadTestCase):
|
||||
"""Test "hello_world" view."""
|
||||
def test_download_hello_world(self):
|
||||
"""hello_world view returns hello-world.txt as attachement."""
|
||||
download_url = reverse('hello_world')
|
||||
response = self.client.get(download_url)
|
||||
self.assertDownloadHelloWorld(response)
|
||||
|
||||
|
||||
class CustomPathDownloadViewTestCase(DownloadTestCase):
|
||||
"""Test "fixture_from_path" view."""
|
||||
def test_download_hello_world(self):
|
||||
"""fixture_from_path view returns hello-world.txt as attachement."""
|
||||
download_url = reverse('fixture_from_path', args=['hello-world.txt'])
|
||||
response = self.client.get(download_url)
|
||||
self.assertDownloadHelloWorld(response)
|
||||
|
||||
|
||||
class StorageDownloadViewTestCase(DownloadTestCase):
|
||||
"""Test "fixture_from_storage" view."""
|
||||
def test_download_hello_world(self):
|
||||
"""fixture_from_storage view returns hello-world.txt as attachement."""
|
||||
download_url = reverse('fixture_from_storage',
|
||||
args=['hello-world.txt'])
|
||||
response = self.client.get(download_url)
|
||||
self.assertDownloadHelloWorld(response)
|
||||
|
||||
|
||||
class ObjectDownloadViewTestCase(DownloadTestCase):
|
||||
"""Test generic ObjectDownloadView."""
|
||||
@temporary_media_root()
|
||||
def test_download_hello_world(self):
|
||||
"""'download_document' view returns hello-world.txt as attachement."""
|
||||
slug = 'hello-world'
|
||||
download_url = reverse('download_document', kwargs={'slug': slug})
|
||||
document = Document.objects.create(
|
||||
slug=slug,
|
||||
file=File(open(self.files['hello-world.txt'])),
|
||||
)
|
||||
slug = 'hello-world'
|
||||
download_url = reverse('document', kwargs={'slug': slug})
|
||||
Document.objects.create(slug=slug,
|
||||
file=File(open(self.files['hello-world.txt'])))
|
||||
response = self.client.get(download_url)
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertEquals(response['Content-Type'],
|
||||
'text/plain; charset=utf-8')
|
||||
self.assertFalse('ContentEncoding' in response)
|
||||
self.assertEquals(response['Content-Disposition'],
|
||||
'attachment; filename=hello-world.txt')
|
||||
self.assertEqual(open(self.files['hello-world.txt']).read(),
|
||||
response.content)
|
||||
self.assertDownloadHelloWorld(response)
|
||||
|
||||
|
||||
class GeneratedDownloadViewTestCase(DownloadTestCase):
|
||||
"""Test "generated_hello_world" view."""
|
||||
def test_download_hello_world(self):
|
||||
"""generated_hello_world view returns hello-world.txt as attachement.
|
||||
|
||||
"""
|
||||
download_url = reverse('generated_hello_world')
|
||||
response = self.client.get(download_url)
|
||||
self.assertDownloadHelloWorld(response)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,27 @@
|
|||
# coding=utf8
|
||||
"""URL mapping."""
|
||||
from django.conf.urls import patterns, include, url
|
||||
from django.conf.urls import patterns, url
|
||||
|
||||
|
||||
urlpatterns = patterns('demoproject.download.views',
|
||||
url(r'^hello-world\.txt$', 'download_hello_world',
|
||||
name='download_hello_world'),
|
||||
url(r'^document/(?P<slug>[a-zA-Z0-9_-]+)/$', 'download_document',
|
||||
name='download_document'),
|
||||
urlpatterns = patterns(
|
||||
'demoproject.download.views',
|
||||
# Model-based downloads.
|
||||
url(r'^document/(?P<slug>[a-zA-Z0-9_-]+)/$',
|
||||
'download_document',
|
||||
name='document'),
|
||||
# Storage-based downloads.
|
||||
url(r'^storage/(?P<path>[a-zA-Z0-9_-]+\.[a-zA-Z0-9]{1,4})$',
|
||||
'download_fixture_from_storage',
|
||||
name='fixture_from_storage'),
|
||||
# Path-based downloads.
|
||||
url(r'^hello-world\.txt$',
|
||||
'download_hello_world',
|
||||
name='hello_world'),
|
||||
url(r'^path/(?P<path>[a-zA-Z0-9_-]+\.[a-zA-Z0-9]{1,4})$',
|
||||
'download_fixture_from_path',
|
||||
name='fixture_from_path'),
|
||||
# Generated downloads.
|
||||
url(r'^generated/hello-world\.txt$',
|
||||
'download_generated_hello_world',
|
||||
name='generated_hello_world'),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,16 +1,78 @@
|
|||
# coding=utf8
|
||||
"""Demo download views."""
|
||||
from cStringIO import StringIO
|
||||
from os.path import abspath, dirname, join
|
||||
|
||||
from django_downloadview import DownloadView, ObjectDownloadView
|
||||
from django.core.files.storage import FileSystemStorage
|
||||
|
||||
from django_downloadview.files import VirtualFile
|
||||
from django_downloadview.views import (ObjectDownloadView,
|
||||
PathDownloadView,
|
||||
StorageDownloadView,
|
||||
VirtualDownloadView)
|
||||
|
||||
from demoproject.download.models import Document
|
||||
|
||||
|
||||
# Some initializations.
|
||||
|
||||
#: Directory containing code of :py:module:`demoproject.download.views`.
|
||||
app_dir = dirname(abspath(__file__))
|
||||
|
||||
#: Directory containing files fixtures.
|
||||
fixtures_dir = join(app_dir, 'fixtures')
|
||||
hello_world_file = join(fixtures_dir, 'hello-world.txt')
|
||||
|
||||
#: Path to a text file that says 'Hello world!'.
|
||||
hello_world_path = join(fixtures_dir, 'hello-world.txt')
|
||||
|
||||
#: Storage for fixtures.
|
||||
fixtures_storage = FileSystemStorage(location=fixtures_dir)
|
||||
|
||||
|
||||
download_hello_world = DownloadView.as_view(filename=hello_world_file,
|
||||
storage=None)
|
||||
# Here are the views.
|
||||
|
||||
#: Pre-configured download view for :py:class:`Document` model.
|
||||
download_document = ObjectDownloadView.as_view(model=Document)
|
||||
|
||||
|
||||
#: Pre-configured view using a storage.
|
||||
download_fixture_from_storage = StorageDownloadView.as_view(
|
||||
storage=fixtures_storage)
|
||||
|
||||
|
||||
#: Direct download of one file, based on an absolute path.
|
||||
#:
|
||||
#: You could use this example as a shortcut, inside other views.
|
||||
download_hello_world = PathDownloadView.as_view(path=hello_world_path)
|
||||
|
||||
|
||||
class CustomPathDownloadView(PathDownloadView):
|
||||
"""Example of customized PathDownloadView."""
|
||||
def get_path(self):
|
||||
"""Convert relative path (provided in URL) into absolute path.
|
||||
|
||||
Notice that this particularly simple use case is covered by
|
||||
:py:class:`django_downloadview.views.StorageDownloadView`.
|
||||
|
||||
.. warning::
|
||||
|
||||
If you are doing such things, make the path secure! Prevent users
|
||||
to download files anywhere in the filesystem.
|
||||
|
||||
"""
|
||||
path = super(CustomPathDownloadView, self).get_path()
|
||||
return join(fixtures_dir, path)
|
||||
|
||||
#: Pre-configured :py:class:`CustomPathDownloadView`.
|
||||
download_fixture_from_path = CustomPathDownloadView.as_view()
|
||||
|
||||
|
||||
class StringIODownloadView(VirtualDownloadView):
|
||||
"""Sample download view using StringIO object."""
|
||||
def get_file(self):
|
||||
"""Return wrapper on StringIO object."""
|
||||
file_obj = StringIO(u"Hello world!\n")
|
||||
return VirtualFile(file_obj, name='hello-world.txt')
|
||||
|
||||
#: Pre-configured view that serves "Hello world!" via a StringIO.
|
||||
download_generated_hello_world = StringIODownloadView.as_view()
|
||||
|
|
|
|||
|
|
@ -65,14 +65,14 @@ MIDDLEWARE_CLASSES = [
|
|||
#NGINX_DOWNLOAD_MIDDLEWARE_MEDIA_URL = "/proxied-download"
|
||||
|
||||
|
||||
# Development configuratio.
|
||||
# Development configuration.
|
||||
DEBUG = True
|
||||
TEMPLATE_DEBUG = DEBUG
|
||||
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
|
||||
NOSE_ARGS = ['--verbose',
|
||||
'--nocapture',
|
||||
'--rednose',
|
||||
'--with-id', # allows --failed which only reruns failed tests
|
||||
'--with-id', # allows --failed which only reruns failed tests
|
||||
'--id-file=%s' % join(data_dir, 'test', 'noseids'),
|
||||
'--with-doctest',
|
||||
'--with-xunit',
|
||||
|
|
@ -81,4 +81,5 @@ NOSE_ARGS = ['--verbose',
|
|||
'--cover-erase',
|
||||
'--cover-package=django_downloadview',
|
||||
'--no-path-adjustment',
|
||||
]
|
||||
'--all-modules',
|
||||
]
|
||||
|
|
|
|||
|
|
@ -7,9 +7,21 @@
|
|||
<h1>Welcome to django-downloadview demo!</h1>
|
||||
<p>Here are some demo links. Browse the code to see how they are implemented</p>
|
||||
<ul>
|
||||
<li><a href="{% url 'download_hello_world' %}">DownloadView</a></li>
|
||||
<li><a href="{% url 'download_document' 'hello-world' %}">ObjectDownloadView</a></li>
|
||||
<li><a href="{% url 'download_document_nginx' 'hello-world' %}">ObjectDownloadView decorated with nginx X-Accel-Redirect</a> (better if served behind nginx)</li>
|
||||
<li><a href="{% url 'hello_world' %}">
|
||||
Direct download to one file using PathDownloadView.
|
||||
</a></li>
|
||||
<li><a href="{% url 'fixture_from_path' 'hello-world.txt' %}">
|
||||
Download files using PathDownloadView and relative path in URL.
|
||||
</a></li>
|
||||
<li><a href="{% url 'fixture_from_storage' 'hello-world.txt' %}">
|
||||
Download files using StorageDownloadView and path in URL.
|
||||
</a></li>
|
||||
<li><a href="{% url 'document' 'hello-world' %}">
|
||||
ObjectDownloadView
|
||||
</a></li>
|
||||
<li><a href="{% url 'download_document_nginx' 'hello-world' %}">
|
||||
ObjectDownloadView decorated with nginx X-Accel-Redirect
|
||||
</a> to be served behind Nginx</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
13
demo/demoproject/tests.py
Normal file
13
demo/demoproject/tests.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# coding=utf8
|
||||
"""Test suite for demoproject.download."""
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class HomeViewTestCase(TestCase):
|
||||
"""Test homepage."""
|
||||
def test_get(self):
|
||||
"""Homepage returns HTTP 200."""
|
||||
home_url = reverse('home')
|
||||
response = self.client.get(home_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
|
@ -5,11 +5,12 @@ from django.views.generic import TemplateView
|
|||
home = TemplateView.as_view(template_name='home.html')
|
||||
|
||||
|
||||
urlpatterns = patterns('',
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
# Standard download views.
|
||||
url(r'^download/', include('demoproject.download.urls')),
|
||||
# Nginx optimizations.
|
||||
url(r'^nginx/', include('demoproject.nginx.urls')),
|
||||
# An informative page.
|
||||
# An informative homepage.
|
||||
url(r'', home, name='home')
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
"""django-downloadview provides generic download views for Django."""
|
||||
from django_downloadview.views import DownloadView, ObjectDownloadView
|
||||
# Shortcut import.
|
||||
from django_downloadview.views import (PathDownloadView,
|
||||
ObjectDownloadView,
|
||||
StorageDownloadView,
|
||||
VirtualDownloadView)
|
||||
|
||||
|
||||
pkg_resources = __import__('pkg_resources')
|
||||
|
|
|
|||
168
django_downloadview/files.py
Normal file
168
django_downloadview/files.py
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
"""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)
|
||||
|
||||
|
||||
class VirtualFile(File):
|
||||
def __init__(self, file=None, name=u'', url='', size=None):
|
||||
"""Constructor.
|
||||
|
||||
file:
|
||||
File object. Typically a StringIO.
|
||||
|
||||
name:
|
||||
File basename.
|
||||
|
||||
url:
|
||||
File URL.
|
||||
|
||||
"""
|
||||
super(VirtualFile, self).__init__(file, name)
|
||||
self.url = url
|
||||
if size is not None:
|
||||
self._size = size
|
||||
|
||||
def _get_size(self):
|
||||
try:
|
||||
return self._size
|
||||
except AttributeError:
|
||||
try:
|
||||
self._size = self.file.size
|
||||
except AttributeError:
|
||||
self._size = len(self.file.getvalue())
|
||||
return self._size
|
||||
|
||||
def _set_size(self, value):
|
||||
return super(VirtualFile, self)._set_size(value)
|
||||
|
||||
size = property(_get_size, _set_size)
|
||||
|
|
@ -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,161 +27,111 @@ 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):
|
||||
"""Download a file from storage and filename."""
|
||||
#: Server-side name (including path) of the file to serve.
|
||||
#:
|
||||
#: If ``storage`` is not None, then the filename will be passed to the
|
||||
#: storage, else filename is supposed to be an absolute filename of a file
|
||||
#: located on the local filesystem.
|
||||
filename = None
|
||||
|
||||
#: Storage to use to fetch the file.
|
||||
#:
|
||||
#: Defaults to Django's DefaultStorage(), which itself defaults to a
|
||||
#: FileSystemStorage relative to settings.MEDIA_ROOT.
|
||||
#:
|
||||
#: The ``storage`` can be set to None, but you should use one. As an
|
||||
#: example, storage classes may encapsulate some security checks
|
||||
#: (FileSystemStorage actually refuses to serve files outside its root
|
||||
#: location).
|
||||
storage = DefaultStorage()
|
||||
|
||||
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()
|
||||
|
||||
class BaseDownloadView(DownloadMixin, View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Handle GET requests: stream a file."""
|
||||
return self.render_to_response()
|
||||
|
||||
|
||||
class PathDownloadView(BaseDownloadView):
|
||||
"""Serve a file using filename."""
|
||||
#: Server-side name (including path) of the file to serve.
|
||||
#:
|
||||
#: Filename is supposed to be an absolute filename of a file located on the
|
||||
#: local filesystem.
|
||||
path = None
|
||||
|
||||
#: Name of the URL argument that contains path.
|
||||
path_url_kwarg = 'path'
|
||||
|
||||
def get_path(self):
|
||||
"""Return actual path of the file to serve.
|
||||
|
||||
Default implementation simply returns view's :py:attr:`path`.
|
||||
|
||||
Override this method if you want custom implementation.
|
||||
As an example, :py:attr:`path` could be relative and your custom
|
||||
:py:meth:`get_path` implementation makes it absolute.
|
||||
|
||||
"""
|
||||
return self.kwargs.get(self.path_url_kwarg, self.path)
|
||||
|
||||
def get_file(self):
|
||||
"""Use path to return wrapper around file to serve."""
|
||||
return File(open(self.get_path()))
|
||||
|
||||
|
||||
class StorageDownloadView(PathDownloadView):
|
||||
"""Serve a file using storage and filename."""
|
||||
#: Storage the file to serve belongs to.
|
||||
storage = DefaultStorage()
|
||||
|
||||
#: Path to the file to serve relative to storage.
|
||||
path = None # Override docstring.
|
||||
|
||||
def get_path(self):
|
||||
"""Return path of the file to serve, relative to storage.
|
||||
|
||||
Default implementation simply returns view's :py:attr:`path`.
|
||||
|
||||
Override this method if you want custom implementation.
|
||||
|
||||
"""
|
||||
return super(StorageDownloadView, self).get_path()
|
||||
|
||||
def get_file(self):
|
||||
"""Use path and storage to return wrapper around file to serve."""
|
||||
return StorageFile(self.storage, self.get_path())
|
||||
|
||||
|
||||
class VirtualDownloadView(BaseDownloadView):
|
||||
"""Serve not-on-disk or generated-on-the-fly file.
|
||||
|
||||
Use this class to serve :py:class:`StringIO` files.
|
||||
|
||||
Override the :py:meth:`get_file` method to customize file wrapper.
|
||||
|
||||
"""
|
||||
def get_file(self):
|
||||
"""Return wrapper."""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class ObjectDownloadView(DownloadMixin, BaseDetailView):
|
||||
"""Download view for models which contain a FileField.
|
||||
|
||||
|
|
@ -227,75 +173,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
|
||||
-------------------------
|
||||
|
||||
|
|
|
|||
|
|
@ -138,7 +138,12 @@ html_static_path = ['_static']
|
|||
#html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#html_sidebars = {}
|
||||
html_sidebars = {
|
||||
'**': ['globaltoc.html',
|
||||
'relations.html',
|
||||
'sourcelink.html',
|
||||
'searchbox.html'],
|
||||
}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
|
|
|
|||
|
|
@ -10,16 +10,8 @@ Contents
|
|||
|
||||
demo
|
||||
install
|
||||
views
|
||||
optimizations/index
|
||||
api/index
|
||||
about/index
|
||||
dev
|
||||
|
||||
|
||||
******************
|
||||
Indices and tables
|
||||
******************
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
|
|
|||
59
docs/views.txt
Normal file
59
docs/views.txt
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
##############
|
||||
Download views
|
||||
##############
|
||||
|
||||
This section contains narrative overview about class-based views provided by
|
||||
django-downloadview.
|
||||
|
||||
|
||||
******************
|
||||
ObjectDownloadView
|
||||
******************
|
||||
|
||||
The :py:class:`django_downloadview.views.ObjectDownloadView` class-based view
|
||||
allows you to **serve files given a model with some file fields** such as
|
||||
FileField or ImageField.
|
||||
|
||||
Use this view anywhere you could use Django's builtin ObjectDetailView.
|
||||
|
||||
Some options allow you to store file metadata (size, content-type, ...) in the
|
||||
model, as deserialized fields.
|
||||
|
||||
|
||||
*******************
|
||||
StorageDownloadView
|
||||
*******************
|
||||
|
||||
The :py:class:`django_downloadview.views.StorageDownloadView` class-based view
|
||||
allows you to **serve files given a storage and a path**.
|
||||
|
||||
Use this view when you manage files in a storage (which is a good practice),
|
||||
unrelated to a model.
|
||||
|
||||
|
||||
****************
|
||||
PathDownloadView
|
||||
****************
|
||||
|
||||
The :py:class:`django_downloadview.views.PathDownloadView` class-based view
|
||||
allows you to **serve files given an absolute path on local filesystem**.
|
||||
|
||||
Two main use cases:
|
||||
|
||||
* as a shortcut. This dead-simple view is straight to call, so you can use it
|
||||
to simplify code in more complex views, provided you have an absolute path to
|
||||
a local file.
|
||||
|
||||
* override. Extend :py:class:`django_downloadview.views.PathDownloadView` and
|
||||
override :py:meth:`django_downloadview.views.PathDownloadView:get_path`.
|
||||
|
||||
|
||||
*******************
|
||||
VirtualDownloadView
|
||||
*******************
|
||||
|
||||
The :py:class:`django_downloadview.views.VirtualDownloadView` class-based view
|
||||
allows you to **serve files that don't live on disk**.
|
||||
|
||||
Use it when you want to stream a file which content is dynamically generated
|
||||
or which lives in memory.
|
||||
|
|
@ -7,10 +7,15 @@ versions = versions
|
|||
# of in current directory.
|
||||
bin-directory = bin
|
||||
develop-eggs-directory = lib/buildout/develop-eggs
|
||||
downloads-directory = lib/buildout/downloads
|
||||
eggs-directory = lib/buildout/eggs
|
||||
installed = lib/buildout/.installed.cfg
|
||||
parts-directory = lib/buildout/parts
|
||||
# Package index, mirrors, allowed hosts and dependency links. Those options
|
||||
# control locations where buildout looks for packages.
|
||||
index = http://f.pypi.python.org/simple
|
||||
find-links =
|
||||
allow-hosts = *.python.org
|
||||
use-dependency-links = false
|
||||
# Development.
|
||||
develop =
|
||||
${buildout:directory}/
|
||||
|
|
@ -29,7 +34,8 @@ eggs =
|
|||
rednose
|
||||
coverage
|
||||
sphinx
|
||||
initialization =
|
||||
sphinxcontrib-testbuild
|
||||
initialization =
|
||||
import os
|
||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'demoproject.settings'
|
||||
|
||||
|
|
@ -45,19 +51,21 @@ recipe = z3c.recipe.scripts
|
|||
eggs = zest.releaser
|
||||
|
||||
[versions]
|
||||
Django = 1.4.2
|
||||
Django = 1.4.3
|
||||
Jinja2 = 2.6
|
||||
Pygments = 1.6
|
||||
Sphinx = 1.1.3
|
||||
bpython = 0.11
|
||||
bpython = 0.10.1
|
||||
buildout-versions = 1.7
|
||||
coverage = 3.5.2
|
||||
nose = 1.1.2
|
||||
coverage = 3.6
|
||||
distribute = 0.6.34
|
||||
docutils = 0.10
|
||||
nose = 1.2.1
|
||||
python-termstyle = 0.1.9
|
||||
rednose = 0.3
|
||||
sphinxcontrib-testbuild = 0.1.1
|
||||
z3c.recipe.mkdir = 0.5
|
||||
z3c.recipe.scripts = 1.0.1
|
||||
zc.recipe.egg = 1.3.2
|
||||
zest.releaser = 3.37
|
||||
docutils = 0.9.1
|
||||
Pygments = 1.5
|
||||
zest.releaser = 3.43
|
||||
django-nose = 1.1
|
||||
7
etc/nose.cfg
Normal file
7
etc/nose.cfg
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[nosetests]
|
||||
verbosity = 2
|
||||
nocapture = True
|
||||
with-doctest = True
|
||||
rednose = True
|
||||
no-path-adjustment = True
|
||||
all-modules = True
|
||||
Loading…
Reference in a new issue