Merge pull request #30 from benoitbryon/29-httpdownloadview

Introducing HTTPDownloadView
This commit is contained in:
Benoît Bryon 2013-04-08 07:45:29 -07:00
commit aa73e2a47a
11 changed files with 144 additions and 28 deletions

View file

@ -6,6 +6,10 @@ Changelog
**Backward incompatible changes.**
- Added HTTPDownloadView to proxy to arbitrary URL.
- Added VirtualDownloadView to support files living in memory.
- Using StreamingHttpResponse introduced with Django 1.5. Makes Django 1.5 a
requirement!

View file

@ -87,3 +87,12 @@ class GeneratedDownloadViewTestCase(DownloadTestCase):
download_url = reverse('generated_hello_world')
response = self.client.get(download_url)
self.assertDownloadHelloWorld(response)
class ProxiedDownloadViewTestCase(DownloadTestCase):
"""Test "http_hello_world" view."""
def test_download_readme(self):
"""http_hello_world view proxies file from URL."""
download_url = reverse('http_hello_world')
response = self.client.get(download_url)
self.assertDownloadHelloWorld(response)

View file

@ -20,6 +20,10 @@ urlpatterns = patterns(
url(r'^path/(?P<path>[a-zA-Z0-9_-]+\.[a-zA-Z0-9]{1,4})$',
'download_fixture_from_path',
name='fixture_from_path'),
# URL-based downloads.
url(r'^http/readme\.txt$',
'download_http_hello_world',
name='http_hello_world'),
# Generated downloads.
url(r'^generated/hello-world\.txt$',
'download_generated_hello_world',

View file

@ -6,11 +6,7 @@ from os.path import abspath, dirname, join
from django.core.files.storage import FileSystemStorage
from django_downloadview.files import VirtualFile
from django_downloadview.views import (ObjectDownloadView,
PathDownloadView,
StorageDownloadView,
VirtualDownloadView)
from django_downloadview import views
from demoproject.download.models import Document
@ -32,21 +28,21 @@ fixtures_storage = FileSystemStorage(location=fixtures_dir)
# Here are the views.
#: Pre-configured download view for :py:class:`Document` model.
download_document = ObjectDownloadView.as_view(model=Document)
download_document = views.ObjectDownloadView.as_view(model=Document)
#: Pre-configured view using a storage.
download_fixture_from_storage = StorageDownloadView.as_view(
download_fixture_from_storage = views.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)
download_hello_world = views.PathDownloadView.as_view(path=hello_world_path)
class CustomPathDownloadView(PathDownloadView):
class CustomPathDownloadView(views.PathDownloadView):
"""Example of customized PathDownloadView."""
def get_path(self):
"""Convert relative path (provided in URL) into absolute path.
@ -67,12 +63,17 @@ class CustomPathDownloadView(PathDownloadView):
download_fixture_from_path = CustomPathDownloadView.as_view()
class StringIODownloadView(VirtualDownloadView):
class StringIODownloadView(views.VirtualDownloadView):
"""Sample download view using StringIO object."""
def get_file(self):
"""Return wrapper on StringIO object."""
file_obj = StringIO(u"Hello world!\n")
file_obj = StringIO(u"Hello world!\n".encode('utf-8'))
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()
download_http_hello_world = views.HTTPDownloadView.as_view(
url=u'https://raw.github.com/benoitbryon/django-downloadview/master/demo/demoproject/download/fixtures/hello-world.txt',
name=u'hello-world.txt')

View file

@ -1,5 +1,5 @@
"""URL mapping."""
from django.conf.urls import patterns, include, url
from django.conf.urls import patterns, url
urlpatterns = patterns('demoproject.nginx.views',

View file

@ -1,9 +1,10 @@
"""Views."""
from django_downloadview.nginx import x_accel_redirect
from demoproject.download.views import download_document
from demoproject.download import views
download_document_nginx = x_accel_redirect(download_document,
media_root='/var/www/files',
media_url='/download-optimized')
download_document_nginx = x_accel_redirect(
views.download_document,
source_dir='/var/www/files',
destination_url='/download-optimized')

View file

@ -5,23 +5,35 @@
</head>
<body>
<h1>Welcome to django-downloadview demo!</h1>
<p>Here are some demo links. Browse the code to see how they are implemented</p>
<p>Here are some demo links. Browse the code to see how they are implemented</p>
<h2>Serving files with Django</h2>
<p>In the following views, Django streams the files, no optimization
has been setup.</p>
<ul>
<li><a href="{% url 'hello_world' %}">
Direct download to one file using PathDownloadView.
</a></li>
<li><a href="{% url 'hello_world' %}">PathDownloadView</a></li>
<li><a href="{% url 'fixture_from_path' 'hello-world.txt' %}">
Download files using PathDownloadView and relative path in URL.
PathDownloadView + argument in URL
</a></li>
<li><a href="{% url 'fixture_from_storage' 'hello-world.txt' %}">
Download files using StorageDownloadView and path in URL.
StorageDownloadView + path in URL
</a></li>
<li><a href="{% url 'document' 'hello-world' %}">
ObjectDownloadView
</a></li>
<li><a href="{% url 'http_hello_world' %}">
HTTPDownloadView</a>, a simple HTTP proxy</li>
</ul>
<h2>Optimized downloads</h2>
<p>In the following views, Django delegates actual streaming to another
server, for improved performances.</p>
<p>Since nginx and other servers aren't installed on the demo, you
will get raw "X-Sendfile" responses. Look at the headers!</p>
<ul>
<li><a href="{% url 'download_document_nginx' 'hello-world' %}">
ObjectDownloadView decorated with nginx X-Accel-Redirect
</a> to be served behind Nginx</li>
ObjectDownloadView (nginx)
</a></li>
</ul>
</body>
</html>

View file

@ -1,6 +1,8 @@
"""File wrappers for use as exchange data between views and responses."""
from django.core.files import File
import requests
class StorageFile(File):
"""A file in a Django storage.
@ -134,11 +136,12 @@ class StorageFile(File):
class VirtualFile(File):
"""Wrapper for files that live in memory."""
def __init__(self, file=None, name=u'', url='', size=None):
"""Constructor.
file:
File object. Typically a StringIO.
File object. Typically an io.StringIO.
name:
File basename.
@ -166,3 +169,44 @@ class VirtualFile(File):
return super(VirtualFile, self)._set_size(value)
size = property(_get_size, _set_size)
class HTTPFile(File):
"""Wrapper for files that live on remote HTTP servers.
Acts as a proxy.
Uses https://pypi.python.org/pypi/requests.
Always sets "stream=True" in requests kwargs.
"""
def __init__(self, request_factory=requests.get, url='', name=u'',
**kwargs):
self.request_factory = request_factory
self.url = url
self.name = name
kwargs['stream'] = True
self.request_kwargs = kwargs
@property
def request(self):
try:
return self._request
except AttributeError:
self._request = self.request_factory(self.url,
**self.request_kwargs)
return self._request
@property
def file(self):
return self.request.raw
@property
def size(self):
"""Return the total size, in bytes, of the file.
Reads response's "content-length" header.
"""
return self.request.headers['Content-Length']

View file

@ -6,7 +6,7 @@ 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 import files
from django_downloadview.response import DownloadResponse
@ -116,7 +116,7 @@ class StorageDownloadView(PathDownloadView):
def get_file(self):
"""Use path and storage to return wrapper around file to serve."""
return StorageFile(self.storage, self.get_path())
return files.StorageFile(self.storage, self.get_path())
class VirtualDownloadView(BaseDownloadView):
@ -132,6 +132,22 @@ class VirtualDownloadView(BaseDownloadView):
raise NotImplementedError()
class HTTPDownloadView(BaseDownloadView):
"""Proxy files that live on remote servers."""
url = u''
request_kwargs = {}
name = u''
def get_url(self):
return self.url
def get_file(self):
"""Return wrapper which has an ``url`` attribute."""
url = self.get_url()
request_kwargs = self.request_kwargs
return files.HTTPFile(name=self.name, url=url, **request_kwargs)
class ObjectDownloadView(DownloadMixin, BaseDetailView):
"""Download view for models which contain a FileField.

View file

@ -48,6 +48,24 @@ Two main use cases:
override :py:meth:`django_downloadview.views.PathDownloadView:get_path`.
****************
HTTPDownloadView
****************
The :py:class:`django_downloadview.views.HTTPDownloadView` class-based view
allows you to **serve files given an URL**. That URL is supposed to be
downloadable from the Django server.
Use it when you want to setup a proxy to remote files:
* the Django view filters input and computes target URL.
* if you setup optimizations, Django itself doesn't proxies the file,
* but, as a fallback, Django uses `requests`_ to proxy the file.
Extend :py:class:`django_downloadview.views.HTTPDownloadView` then
override :py:meth:`django_downloadview.views.HTTPDownloadView:get_url`.
*******************
VirtualDownloadView
*******************
@ -57,3 +75,10 @@ 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.
.. rubric:: References
.. target-notes::
.. _`requests`: https://pypi.python.org/pypi/requests

View file

@ -15,7 +15,7 @@ NAME = 'django-downloadview'
README = read_relative_file('README')
VERSION = read_relative_file('VERSION')
PACKAGES = ['django_downloadview']
REQUIRES = ['setuptools', 'django>=1.5']
REQUIRES = ['setuptools', 'django>=1.5', 'requests']
if __name__ == '__main__': # Don't run setup() when we import this module.