From 4ee0e3c5710eac14da65dc680ef11bf61385540c Mon Sep 17 00:00:00 2001 From: Josip Lazic Date: Mon, 2 Nov 2015 20:43:54 +0100 Subject: [PATCH 01/12] Implement inline attachment --- sendfile/__init__.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/sendfile/__init__.py b/sendfile/__init__.py index 2e707ba..83c2f20 100644 --- a/sendfile/__init__.py +++ b/sendfile/__init__.py @@ -8,13 +8,16 @@ 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 +38,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 +53,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,12 +66,15 @@ 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 @@ -77,7 +82,7 @@ def sendfile(request, filename, attachment=False, attachment_filename=None, mime # 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') + 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 From 99239528aeea2b3dd7e9103ffbd6f36a8d6268bc Mon Sep 17 00:00:00 2001 From: Josip Lazic Date: Mon, 2 Nov 2015 20:50:09 +0100 Subject: [PATCH 02/12] Implement inline attachment in Content-Disposition --- README.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 7e4b8f4..d188667 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: From d9132448fa25f9a8960d510e354499ab998b4d8b Mon Sep 17 00:00:00 2001 From: Johannes Linke Date: Thu, 17 Dec 2015 14:02:02 +0100 Subject: [PATCH 03/12] Prevent file leak in 'simple' backend --- sendfile/backends/simple.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 From 7841251c4ed50c5ce3531778f3442107668565ff Mon Sep 17 00:00:00 2001 From: Moritz Pfeiffer Date: Thu, 30 Jun 2016 12:18:44 +0200 Subject: [PATCH 04/12] Added url quoting to url written to X-Accel-Redirect and Location headers. --- sendfile/backends/_internalredirect.py | 14 +++++++++++-- sendfile/tests.py | 29 +++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/sendfile/backends/_internalredirect.py b/sendfile/backends/_internalredirect.py index a6a42ee..76bc306 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 force_text, force_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 force bytes for quoting and then go back to unicode. + url = [force_bytes(url_component) for url_component in url] + return force_text(quote(b'/'.join(url))) 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'])) From 14f13b0abbb2dee2bf177bf8082887961e0daf93 Mon Sep 17 00:00:00 2001 From: Moritz Pfeiffer Date: Mon, 4 Jul 2016 10:38:31 +0200 Subject: [PATCH 05/12] Changed encoding utils to smart_text, smart_bytes to support Django >= 1.4.2. --- sendfile/backends/_internalredirect.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sendfile/backends/_internalredirect.py b/sendfile/backends/_internalredirect.py index 76bc306..be4e069 100644 --- a/sendfile/backends/_internalredirect.py +++ b/sendfile/backends/_internalredirect.py @@ -1,7 +1,7 @@ import os.path from django.conf import settings -from django.utils.encoding import force_text, force_bytes +from django.utils.encoding import smart_text, smart_bytes try: from urllib.parse import quote @@ -19,6 +19,6 @@ def _convert_file_to_url(filename): url.insert(1, head) # Python3 urllib.parse.quote accepts both unicode and bytes, while Python2 urllib.quote only accepts bytes. - # So force bytes for quoting and then go back to unicode. - url = [force_bytes(url_component) for url_component in url] - return force_text(quote(b'/'.join(url))) + # 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))) From 7a000e0e7ae1732bcea222c7872bfda6a97c495c Mon Sep 17 00:00:00 2001 From: John Montgomery Date: Sun, 28 Aug 2016 17:40:14 +0100 Subject: [PATCH 06/12] Version 0.3.11 --- sendfile/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sendfile/__init__.py b/sendfile/__init__.py index 2e707ba..1cc9809 100644 --- a/sendfile/__init__.py +++ b/sendfile/__init__.py @@ -1,4 +1,4 @@ -VERSION = (0, 3, 10) +VERSION = (0, 3, 11) __version__ = '.'.join(map(str, VERSION)) import os.path From 4b1ef74e074ea2186e83c36c24ac218e37d0fe1d Mon Sep 17 00:00:00 2001 From: Ellis Percival Date: Thu, 12 Jan 2017 11:30:53 +0000 Subject: [PATCH 07/12] Fix nginx documentation URL --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 7e4b8f4..76ccf99 100644 --- a/README.rst +++ b/README.rst @@ -114,7 +114,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: From 0cbba4049c3712aefffb2e0c3620d33dd68284fb Mon Sep 17 00:00:00 2001 From: Josip Lazic Date: Mon, 2 Nov 2015 20:43:54 +0100 Subject: [PATCH 08/12] Implement inline attachment --- sendfile/__init__.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/sendfile/__init__.py b/sendfile/__init__.py index 1cc9809..9d0cbf4 100644 --- a/sendfile/__init__.py +++ b/sendfile/__init__.py @@ -8,13 +8,16 @@ 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 +38,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 +53,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,12 +66,15 @@ 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 @@ -77,7 +82,7 @@ def sendfile(request, filename, attachment=False, attachment_filename=None, mime # 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') + 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 From c0f909194e366c0eed4c67c5f397c53131db5a71 Mon Sep 17 00:00:00 2001 From: Josip Lazic Date: Mon, 2 Nov 2015 20:50:09 +0100 Subject: [PATCH 09/12] Implement inline attachment in Content-Disposition --- README.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 76ccf99..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: From 664efab2345f59e941e062bdf19aeb3fda250333 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josip=20Lazi=C4=87?= Date: Tue, 6 Jun 2023 11:06:38 +0200 Subject: [PATCH 10/12] Update __init__.py Changes required for Django 3.0+ --- sendfile/__init__.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/sendfile/__init__.py b/sendfile/__init__.py index 9d0cbf4..6e3f60d 100644 --- a/sendfile/__init__.py +++ b/sendfile/__init__.py @@ -1,9 +1,11 @@ -VERSION = (0, 3, 11) +VERSION = (0, 3, 12) __version__ = '.'.join(map(str, VERSION)) import os.path from mimetypes import guess_type import unicodedata +from urllib.parse import quote +from django.utils.encoding import force_str def _lazy_load(fn): @@ -76,17 +78,14 @@ def sendfile(request, filename, attachment=False, inline=False, attachment_filen 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) + + + + 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) From 330f59acf4738dcedf7f711a8e6c7d85cee74c04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josip=20Lazi=C4=87?= Date: Tue, 6 Jun 2023 15:33:28 +0200 Subject: [PATCH 11/12] Update xsendfile.py --- sendfile/backends/xsendfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From fd29c6e13cb92ad42bc6f93a85d1f5c261eb2248 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josip=20Lazi=C4=87?= Date: Wed, 8 Oct 2025 20:04:48 +0200 Subject: [PATCH 12/12] Update __init__.py Lazy load django string function --- sendfile/__init__.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/sendfile/__init__.py b/sendfile/__init__.py index 6e3f60d..9a6ebe3 100644 --- a/sendfile/__init__.py +++ b/sendfile/__init__.py @@ -5,7 +5,6 @@ from mimetypes import guess_type import unicodedata from urllib.parse import quote -from django.utils.encoding import force_str def _lazy_load(fn): @@ -78,9 +77,7 @@ def sendfile(request, filename, attachment=False, inline=False, attachment_filen else: parts = ['attachment'] if attachment_filename: - - - + 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)