Merge branch 'jordij/feature/docs-sendfile'

pull/1278/merge
Karl Hobley 2015-05-07 09:36:11 +01:00
commit c8d7f5baad
7 zmienionych plików z 201 dodań i 15 usunięć

Wyświetl plik

@ -34,6 +34,7 @@ install_requires = [
"django-modelcluster>=0.6",
"django-taggit>=0.13.0",
"django-treebeard==3.0",
"django-sendfile==0.3.7",
"Pillow>=2.6.1",
"beautifulsoup4>=4.3.2",
"html5lib==0.999",

Wyświetl plik

@ -23,6 +23,7 @@ deps =
django-modelcluster>=0.6
django-taggit==0.13.0
django-treebeard==3.0
django-sendfile==0.3.6
Pillow>=2.3.0
beautifulsoup4>=4.3.2
html5lib==0.999

Wyświetl plik

@ -83,6 +83,7 @@ INSTALLED_APPS = (
'taggit',
'compressor',
'sendfile',
'wagtail.wagtailcore',
'wagtail.wagtailadmin',

Wyświetl plik

@ -0,0 +1,96 @@
# Copied from django-sendfile 0.3.6 and tweaked to allow a backend to be passed
# to sendfile()
# See: https://github.com/johnsensible/django-sendfile/pull/33
import os.path
from mimetypes import guess_type
VERSION = (0, 3, 6)
__version__ = '.'.join(map(str, VERSION))
def _lazy_load(fn):
_cached = []
def _decorated():
if not _cached:
_cached.append(fn())
return _cached[0]
def clear():
while _cached:
_cached.pop()
_decorated.clear = clear
return _decorated
@_lazy_load
def _get_sendfile():
from django.utils.importlib import import_module
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
backend = getattr(settings, 'SENDFILE_BACKEND', None)
if not backend:
raise ImproperlyConfigured('You must specify a value for SENDFILE_BACKEND')
module = import_module(backend)
return module.sendfile
def sendfile(request, filename, attachment=False, attachment_filename=None, mimetype=None, encoding=None, backend=None):
'''
create a response to send file using backend configured in SENDFILE_BACKEND
If attachment is True the content-disposition header will be set.
This will typically prompt the user to download the file, rather
than view it. The content-disposition filename depends on the
value of attachment_filename:
None (default): Same as filename
False: No content-disposition filename
String: Value used as filename
If no mimetype or encoding are specified, then they will be guessed via the
filename (using the standard python mimetypes module)
'''
_sendfile = backend or _get_sendfile()
if not os.path.exists(filename):
from django.http import Http404
raise Http404('"%s" does not exist' % filename)
guessed_mimetype, guessed_encoding = guess_type(filename)
if mimetype is None:
if guessed_mimetype:
mimetype = guessed_mimetype
else:
mimetype = 'application/octet-stream'
response = _sendfile(request, filename, mimetype=mimetype)
if attachment:
if attachment_filename is None:
attachment_filename = os.path.basename(filename)
parts = ['attachment']
if attachment_filename:
from unidecode import unidecode
try:
from django.utils.encoding import force_text
except ImportError:
# Django 1.3
from django.utils.encoding import force_unicode as force_text
attachment_filename = force_text(attachment_filename)
ascii_filename = unidecode(attachment_filename)
parts.append('filename="%s"' % ascii_filename)
if ascii_filename != attachment_filename:
from django.utils.http import urlquote
quoted_filename = urlquote(attachment_filename)
parts.append('filename*=UTF-8\'\'%s' % quoted_filename)
response['Content-Disposition'] = '; '.join(parts)
response['Content-length'] = os.path.getsize(filename)
response['Content-Type'] = mimetype
if not encoding:
encoding = guessed_encoding
if encoding:
response['Content-Encoding'] = encoding
return response

Wyświetl plik

@ -0,0 +1,61 @@
# Sendfile "streaming" backend
# This is based on sendfiles builtin "simple" backend but uses a StreamingHttpResponse
import os
import stat
import re
try:
from email.utils import parsedate_tz, mktime_tz
except ImportError:
from email.Utils import parsedate_tz, mktime_tz
from wsgiref.util import FileWrapper
from django.http import StreamingHttpResponse, HttpResponseNotModified
from django.utils.http import http_date
def sendfile(request, filename, **kwargs):
# Respect the If-Modified-Since header.
statobj = os.stat(filename)
if not was_modified_since(request.META.get('HTTP_IF_MODIFIED_SINCE'),
statobj[stat.ST_MTIME], statobj[stat.ST_SIZE]):
return HttpResponseNotModified()
response = StreamingHttpResponse(FileWrapper(open(filename, 'rb')))
response["Last-Modified"] = http_date(statobj[stat.ST_MTIME])
return response
def was_modified_since(header=None, mtime=0, size=0):
"""
Was something modified since the user last downloaded it?
header
This is the value of the If-Modified-Since header. If this is None,
I'll just return True.
mtime
This is the modification time of the item we're talking about.
size
This is the size of the item we're talking about.
"""
try:
if header is None:
raise ValueError
matches = re.match(r"^([^;]+)(; length=([0-9]+))?$", header,
re.IGNORECASE)
header_date = parsedate_tz(matches.group(1))
if header_date is None:
raise ValueError
header_mtime = mktime_tz(header_date)
header_len = matches.group(3)
if header_len and int(header_len) != size:
raise ValueError
if mtime > header_mtime:
raise ValueError
except (AttributeError, ValueError, OverflowError):
return True
return False

Wyświetl plik

@ -4,6 +4,7 @@ from six import b
import unittest
import mock
from bs4 import BeautifulSoup
import os.path
from django.test import TestCase
from django.contrib.auth import get_user_model
@ -11,6 +12,7 @@ from django.contrib.auth.models import Group, Permission
from django.core.urlresolvers import reverse
from django.core.files.base import ContentFile
from django.test.utils import override_settings
from django.conf import settings
from wagtail.tests.utils import WagtailTestUtils
from wagtail.wagtailcore.models import Page
@ -556,6 +558,9 @@ class TestServeView(TestCase):
def test_content_length_header(self):
self.assertEqual(self.get()['Content-Length'], '25')
def test_content_type_header(self):
self.assertEqual(self.get()['Content-Type'], 'application/msword')
def test_is_streaming_response(self):
self.assertTrue(self.get().streaming)
@ -581,6 +586,34 @@ class TestServeView(TestCase):
response = self.client.get(reverse('wagtaildocs_serve', args=(self.document.id, 'incorrectfilename')))
self.assertEqual(response.status_code, 404)
def clear_sendfile_cache(self):
from wagtail.utils.sendfile import _get_sendfile
_get_sendfile.clear()
@override_settings(SENDFILE_BACKEND='sendfile.backends.xsendfile')
def test_sendfile_xsendfile_backend(self):
self.clear_sendfile_cache()
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertEqual(response['X-Sendfile'], os.path.join(settings.MEDIA_ROOT, self.document.file.name))
@override_settings(SENDFILE_BACKEND='sendfile.backends.mod_wsgi', SENDFILE_ROOT=settings.MEDIA_ROOT, SENDFILE_URL=settings.MEDIA_URL[:-1])
def test_sendfile_mod_wsgi_backend(self):
self.clear_sendfile_cache()
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Location'], os.path.join(settings.MEDIA_URL, self.document.file.name))
@override_settings(SENDFILE_BACKEND='sendfile.backends.nginx', SENDFILE_ROOT=settings.MEDIA_ROOT, SENDFILE_URL=settings.MEDIA_URL[:-1])
def test_sendfile_nginx_backend(self):
self.clear_sendfile_cache()
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertEqual(response['X-Accel-Redirect'], os.path.join(settings.MEDIA_URL, self.document.file.name))
class TestServeWithUnicodeFilename(TestCase):
def setUp(self):

Wyświetl plik

@ -1,27 +1,20 @@
from django.shortcuts import get_object_or_404
from wsgiref.util import FileWrapper
from django.http import StreamingHttpResponse, BadHeaderError
from django.conf import settings
from unidecode import unidecode
from wagtail.utils.sendfile import sendfile
from wagtail.utils import sendfile_streaming_backend
from wagtail.wagtaildocs.models import Document, document_served
def serve(request, document_id, document_filename):
doc = get_object_or_404(Document, id=document_id)
wrapper = FileWrapper(doc.file)
response = StreamingHttpResponse(wrapper, content_type='application/octet-stream')
try:
response['Content-Disposition'] = 'attachment; filename=%s' % doc.filename
except BadHeaderError:
# Unicode filenames can fail on Django <1.8, Python 2 due to
# https://code.djangoproject.com/ticket/20889 - try with an ASCIIfied version of the name
response['Content-Disposition'] = 'attachment; filename=%s' % unidecode(doc.filename)
response['Content-Length'] = doc.file.size
# Send document_served signal
document_served.send(sender=Document, instance=doc, request=request)
return response
if hasattr(settings, 'SENDFILE_BACKEND'):
return sendfile(request, doc.file.path, attachment=True, attachment_filename=doc.filename)
else:
# Fallback to streaming backend if user hasn't specified SENDFILE_BACKEND
return sendfile(request, doc.file.path, attachment=True, attachment_filename=doc.filename, backend=sendfile_streaming_backend.sendfile)