Skip to content

Inline attachments #43

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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 <https://www.nginx.com/resources/wiki/start/topics/examples/xsendfile/>`_.

For example though, if I use the django settings:

Expand Down
36 changes: 20 additions & 16 deletions sendfile/__init__.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
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
from django.utils.encoding import force_str


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

Expand All @@ -35,9 +40,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.
Expand All @@ -51,7 +55,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):
Expand All @@ -64,24 +68,24 @@ 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')



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)

Expand Down
14 changes: 12 additions & 2 deletions sendfile/backends/_internalredirect.py
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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)))
3 changes: 2 additions & 1 deletion sendfile/backends/simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion sendfile/backends/xsendfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
29 changes: 28 additions & 1 deletion sendfile/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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']))