Source code for django_sendfile.utils

from functools import lru_cache
from importlib import import_module
from mimetypes import guess_type
from os.path import normpath
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)
    url = normpath(url_root / relpath)

    return quote(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_abs = Path(normpath(path_root / filepath))

    # 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, content_length=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 getattr(settings, "SENDFILE_CHECK_FILE_EXISTS", True) and not filepath_obj.exists(): raise Http404('"%s" does not exist' % filepath_obj) guessed_mimetype, guessed_encoding = guess_type(filepath_obj) if guessed_mimetype is None: guessed_mimetype = 'application/octet-stream' try: response = _sendfile(request, filepath_obj, mimetype=mimetype) except FileNotFoundError: raise Http404('"%s" does not exist' % filepath_obj) # 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) if content_length is not None: response['Content-Length'] = content_length else: try: response['Content-Length'] = filepath_obj.stat().st_size except FileNotFoundError: pass response['Content-Type'] = mimetype or guessed_mimetype encoding = encoding or guessed_encoding if encoding is not None: response['Content-Encoding'] = encoding return response