mirror of
https://github.com/jazzband/django-downloadview.git
synced 2026-04-29 03:04:51 +00:00
Merge pull request #30 from benoitbryon/29-httpdownloadview
Introducing HTTPDownloadView
This commit is contained in:
commit
aa73e2a47a
11 changed files with 144 additions and 28 deletions
|
|
@ -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!
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
2
setup.py
2
setup.py
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue