from functools import lru_cache
from importlib import import_module
from mimetypes import guess_type
from pathlib import Path, PurePath
from urllib.parse import quote
import logging
import unicodedata
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.http import Http404
logger = logging.getLogger(__name__)
@lru_cache(maxsize=None)
def _get_sendfile():
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 _convert_file_to_url(path):
try:
url_root = PurePath(getattr(settings, "SENDFILE_URL", None))
except TypeError:
return path
path_root = PurePath(settings.SENDFILE_ROOT)
path_obj = PurePath(path)
relpath = path_obj.relative_to(path_root)
# Python 3.5: Path.resolve() has no `strict` kwarg, so use pathmod from an
# already instantiated Path object
url = relpath._flavour.pathmod.normpath(str(url_root / relpath))
return quote(str(url))
def _sanitize_path(filepath):
try:
path_root = Path(getattr(settings, 'SENDFILE_ROOT', None))
except TypeError:
raise ImproperlyConfigured('You must specify a value for SENDFILE_ROOT')
filepath_obj = Path(filepath)
# get absolute path
# Python 3.5: Path.resolve() has no `strict` kwarg, so use pathmod from an
# already instantiated Path object
filepath_abs = Path(filepath_obj._flavour.pathmod.normpath(str(path_root / filepath_obj)))
# if filepath_abs is not relative to path_root, relative_to throws an error
try:
filepath_abs.relative_to(path_root)
except ValueError:
raise Http404('{} wrt {} is impossible'.format(filepath_abs, path_root))
return filepath_abs
[docs]def sendfile(request, filename, attachment=False, attachment_filename=None,
mimetype=None, encoding=None):
"""
Create a response to send file using backend configured in ``SENDFILE_BACKEND``
``filename`` is the absolute path to the file to send.
If ``attachment`` is ``True`` the ``Content-Disposition`` header will be set accordingly.
This will typically prompt the user to download the file, rather
than view it. But even if ``False``, the user may still be prompted, depending
on the browser capabilities and configuration.
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 neither ``mimetype`` or ``encoding`` are specified, then they will be guessed via the
filename (using the standard Python mimetypes module)
"""
filepath_obj = _sanitize_path(filename)
logger.debug('filename \'%s\' requested "\
"-> filepath \'%s\' obtained', filename, filepath_obj)
_sendfile = _get_sendfile()
if not filepath_obj.exists():
raise Http404('"%s" does not exist' % filepath_obj)
guessed_mimetype, guessed_encoding = guess_type(str(filepath_obj))
if mimetype is None:
if guessed_mimetype:
mimetype = guessed_mimetype
else:
mimetype = 'application/octet-stream'
response = _sendfile(request, filepath_obj, mimetype=mimetype)
# Suggest to view (inline) or download (attachment) the file
parts = ['attachment' if attachment else 'inline']
if attachment_filename is None:
attachment_filename = filepath_obj.name
if attachment_filename:
attachment_filename = str(attachment_filename).replace("\\", "\\\\").replace('"', r"\"")
ascii_filename = unicodedata.normalize('NFKD', attachment_filename)
ascii_filename = ascii_filename.encode('ascii', 'ignore').decode()
parts.append('filename="%s"' % ascii_filename)
if ascii_filename != attachment_filename:
quoted_filename = quote(attachment_filename)
parts.append('filename*=UTF-8\'\'%s' % quoted_filename)
response['Content-Disposition'] = '; '.join(parts)
response['Content-length'] = filepath_obj.stat().st_size
response['Content-Type'] = mimetype
if not encoding:
encoding = guessed_encoding
if encoding:
response['Content-Encoding'] = encoding
return response