diff --git a/README.rst b/README.rst index 7e4b8f4..19f1896 100644 --- a/README.rst +++ b/README.rst @@ -21,7 +21,8 @@ The interface is a single function `sendfile(request, filename, attachment=False # send myfile.pdf as an attachment with a different name return sendfile(request, '/home/john/myfile.pdf', attachment=True, attachment_filename='full-name.pdf') - + # send myfile.pdf as an inline attachment with a different name + return sendfile(request, '/home/john/myfile.pdf', attachment=True, inline=True, attachment_filename='full-name.pdf') Backends are specified using the setting `SENDFILE_BACKEND`. Currenly available backends are: @@ -114,7 +115,7 @@ As with the mod_wsgi backend you need to set two extra settings: * `SENDFILE_ROOT` - this is a directoy where all files that will be used with sendfile must be located * `SENDFILE_URL` - internal URL prefix for all files served via sendfile -You then need to configure nginx to only allow internal access to the files you wish to serve. More details on this are here http://wiki.nginx.org/XSendfile +You then need to configure nginx to only allow internal access to the files you wish to serve. More details on this `are here `_. For example though, if I use the django settings: diff --git a/sendfile/__init__.py b/sendfile/__init__.py index 2e707ba..9a6ebe3 100644 --- a/sendfile/__init__.py +++ b/sendfile/__init__.py @@ -1,20 +1,24 @@ -VERSION = (0, 3, 10) +VERSION = (0, 3, 12) __version__ = '.'.join(map(str, VERSION)) import os.path from mimetypes import guess_type import unicodedata +from urllib.parse import quote 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 @@ -35,9 +39,8 @@ def _get_sendfile(): return module.sendfile - -def sendfile(request, filename, attachment=False, attachment_filename=None, mimetype=None, encoding=None): - ''' +def sendfile(request, filename, attachment=False, inline=False, attachment_filename=None, mimetype=None, encoding=None): + """ create a response to send file using backend configured in SENDFILE_BACKEND If attachment is True the content-disposition header will be set. @@ -51,7 +54,7 @@ def sendfile(request, filename, attachment=False, attachment_filename=None, mime If no mimetype or encoding are specified, then they will be guessed via the filename (using the standard python mimetypes module) - ''' + """ _sendfile = _get_sendfile() if not os.path.exists(filename): @@ -64,24 +67,22 @@ def sendfile(request, filename, attachment=False, attachment_filename=None, mime 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 inline: + parts = ['inline'] + else: + parts = ['attachment'] if attachment_filename: - 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 = unicodedata.normalize('NFKD', attachment_filename).encode('ascii','ignore') + from django.utils.encoding import force_str + attachment_filename = force_str(attachment_filename) + ascii_filename = unicodedata.normalize('NFKD', attachment_filename).encode('ascii', 'ignore') parts.append('filename="%s"' % ascii_filename) if ascii_filename != attachment_filename: - from django.utils.http import urlquote - quoted_filename = urlquote(attachment_filename) + quoted_filename = quote(attachment_filename) parts.append('filename*=UTF-8\'\'%s' % quoted_filename) response['Content-Disposition'] = '; '.join(parts) diff --git a/sendfile/backends/_internalredirect.py b/sendfile/backends/_internalredirect.py index a6a42ee..be4e069 100644 --- a/sendfile/backends/_internalredirect.py +++ b/sendfile/backends/_internalredirect.py @@ -1,6 +1,13 @@ -from django.conf import settings import os.path +from django.conf import settings +from django.utils.encoding import smart_text, smart_bytes + +try: + from urllib.parse import quote +except ImportError: + from urllib import quote + def _convert_file_to_url(filename): relpath = os.path.relpath(filename, settings.SENDFILE_ROOT) @@ -11,4 +18,7 @@ def _convert_file_to_url(filename): relpath, head = os.path.split(relpath) url.insert(1, head) - return u'/'.join(url) + # Python3 urllib.parse.quote accepts both unicode and bytes, while Python2 urllib.quote only accepts bytes. + # So use bytes for quoting and then go back to unicode. + url = [smart_bytes(url_component) for url_component in url] + return smart_text(quote(b'/'.join(url))) diff --git a/sendfile/backends/simple.py b/sendfile/backends/simple.py index 53b5dc9..9950151 100644 --- a/sendfile/backends/simple.py +++ b/sendfile/backends/simple.py @@ -19,7 +19,8 @@ def sendfile(request, filename, **kwargs): statobj[stat.ST_MTIME], statobj[stat.ST_SIZE]): return HttpResponseNotModified() - response = HttpResponse(File(open(filename, 'rb')).chunks()) + with File(open(filename, 'rb')) as f: + response = HttpResponse(f.chunks()) response["Last-Modified"] = http_date(statobj[stat.ST_MTIME]) return response diff --git a/sendfile/backends/xsendfile.py b/sendfile/backends/xsendfile.py index a87aa83..0ad9bdb 100644 --- a/sendfile/backends/xsendfile.py +++ b/sendfile/backends/xsendfile.py @@ -3,6 +3,6 @@ def sendfile(request, filename, **kwargs): response = HttpResponse() - response['X-Sendfile'] = unicode(filename).encode('utf-8') + response['X-Sendfile'] = str(filename).encode('utf-8') return response diff --git a/sendfile/tests.py b/sendfile/tests.py index ededf3d..0643cae 100644 --- a/sendfile/tests.py +++ b/sendfile/tests.py @@ -9,6 +9,11 @@ import shutil from sendfile import sendfile as real_sendfile, _get_sendfile +try: + from urllib.parse import unquote +except ImportError: + from urllib import unquote + def sendfile(request, filename, **kwargs): # just a simple response with the filename @@ -127,4 +132,26 @@ def test_xaccelredirect_header_containing_unicode(self): filepath = self.ensure_file(u'péter_là_gueule.txt') response = real_sendfile(HttpRequest(), filepath) self.assertTrue(response is not None) - self.assertEqual(u'/private/péter_là_gueule.txt'.encode('utf-8'), response['X-Accel-Redirect']) + self.assertEqual(u'/private/péter_là_gueule.txt'.encode('utf-8'), unquote(response['X-Accel-Redirect'])) + + +class TestModWsgiBackend(TempFileTestCase): + + def setUp(self): + super(TestModWsgiBackend, self).setUp() + settings.SENDFILE_BACKEND = 'sendfile.backends.mod_wsgi' + settings.SENDFILE_ROOT = self.TEMP_FILE_ROOT + settings.SENDFILE_URL = '/private' + _get_sendfile.clear() + + def test_correct_url_in_location_header(self): + filepath = self.ensure_file('readme.txt') + response = real_sendfile(HttpRequest(), filepath) + self.assertTrue(response is not None) + self.assertEqual('/private/readme.txt', response['Location']) + + def test_location_header_containing_unicode(self): + filepath = self.ensure_file(u'péter_là_gueule.txt') + response = real_sendfile(HttpRequest(), filepath) + self.assertTrue(response is not None) + self.assertEqual(u'/private/péter_là_gueule.txt'.encode('utf-8'), unquote(response['Location']))