kopia lustrzana https://github.com/wagtail/wagtail
Merge branch 'jordij/feature/docs-sendfile'
commit
c8d7f5baad
1
setup.py
1
setup.py
|
@ -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",
|
||||
|
|
1
tox.ini
1
tox.ini
|
@ -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
|
||||
|
|
|
@ -83,6 +83,7 @@ INSTALLED_APPS = (
|
|||
|
||||
'taggit',
|
||||
'compressor',
|
||||
'sendfile',
|
||||
|
||||
'wagtail.wagtailcore',
|
||||
'wagtail.wagtailadmin',
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
Ładowanie…
Reference in New Issue