Sendfile: add a sendfile module based on django-sendfile2
parent
070abfcdd8
commit
a19a982b1c
@ -0,0 +1,28 @@
|
||||
Copyright (c) 2011, Sensible Development.
|
||||
Copyright (c) 2019, Matt Molyneaux
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of Django Send File nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
@ -0,0 +1,3 @@
|
||||
Heavily inspired + code borrowed from: https://github.com/moggers87/django-sendfile2/
|
||||
|
||||
We just simplified and inlined it because we don't want another external dependency for distribution packagers to package, as well as need a much simpler version.
|
@ -0,0 +1 @@
|
||||
from .utils import sendfile # noqa
|
@ -0,0 +1,17 @@
|
||||
import os.path
|
||||
|
||||
from django.views.static import serve
|
||||
|
||||
|
||||
def sendfile(request, filename, **kwargs):
|
||||
"""
|
||||
Send file using Django dev static file server.
|
||||
|
||||
.. warning::
|
||||
|
||||
Do not use in production. This is only to be used when developing and
|
||||
is provided for convenience only
|
||||
"""
|
||||
dirname = os.path.dirname(filename)
|
||||
basename = os.path.basename(filename)
|
||||
return serve(request, basename, dirname)
|
@ -0,0 +1,17 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
from django.http import HttpResponse
|
||||
|
||||
from ..utils import _convert_file_to_url
|
||||
|
||||
|
||||
def sendfile(request, filename, **kwargs):
|
||||
response = HttpResponse()
|
||||
response['Location'] = _convert_file_to_url(filename)
|
||||
# need to destroy get_host() to stop django
|
||||
# rewriting our location to include http, so that
|
||||
# mod_wsgi is able to do the internal redirect
|
||||
request.get_host = lambda: ''
|
||||
request.build_absolute_uri = lambda location: location
|
||||
|
||||
return response
|
@ -0,0 +1,12 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
from django.http import HttpResponse
|
||||
|
||||
from ..utils import _convert_file_to_url
|
||||
|
||||
|
||||
def sendfile(request, filename, **kwargs):
|
||||
response = HttpResponse()
|
||||
response['X-Accel-Redirect'] = _convert_file_to_url(filename)
|
||||
|
||||
return response
|
@ -0,0 +1,60 @@
|
||||
from email.utils import mktime_tz, parsedate_tz
|
||||
import re
|
||||
|
||||
from django.core.files.base import File
|
||||
from django.http import HttpResponse, HttpResponseNotModified
|
||||
from django.utils.http import http_date
|
||||
|
||||
|
||||
def sendfile(request, filepath, **kwargs):
|
||||
'''Use the SENDFILE_ROOT value composed with the path arrived as argument
|
||||
to build an absolute path with which resolve and return the file contents.
|
||||
|
||||
If the path points to a file out of the root directory (should cover both
|
||||
situations with '..' and symlinks) then a 404 is raised.
|
||||
'''
|
||||
statobj = filepath.stat()
|
||||
|
||||
# Respect the If-Modified-Since header.
|
||||
if not was_modified_since(request.META.get('HTTP_IF_MODIFIED_SINCE'),
|
||||
statobj.st_mtime, statobj.st_size):
|
||||
return HttpResponseNotModified()
|
||||
|
||||
with File(filepath.open('rb')) as f:
|
||||
response = HttpResponse(f.chunks())
|
||||
|
||||
response["Last-Modified"] = http_date(statobj.st_mtime)
|
||||
return response
|
||||
|
||||
|
||||
def was_modified_since(header=None, mtime=0, size=0):
|
||||
"""
|
||||
Was something modified since the user last downloaded it?
|
||||
|
||||
header
|
||||
This is the value of the If-Modified-Since header. If this is None,
|
||||
I'll just return True.
|
||||
|
||||
mtime
|
||||
This is the modification time of the item we're talking about.
|
||||
|
||||
size
|
||||
This is the size of the item we're talking about.
|
||||
"""
|
||||
try:
|
||||
if header is None:
|
||||
raise ValueError
|
||||
matches = re.match(r"^([^;]+)(; length=([0-9]+))?$", header,
|
||||
re.IGNORECASE)
|
||||
header_date = parsedate_tz(matches.group(1))
|
||||
if header_date is None:
|
||||
raise ValueError
|
||||
header_mtime = mktime_tz(header_date)
|
||||
header_len = matches.group(3)
|
||||
if header_len and int(header_len) != size:
|
||||
raise ValueError
|
||||
if mtime > header_mtime:
|
||||
raise ValueError
|
||||
except (AttributeError, ValueError, OverflowError):
|
||||
return True
|
||||
return False
|
@ -0,0 +1,9 @@
|
||||
from django.http import HttpResponse
|
||||
|
||||
|
||||
def sendfile(request, filename, **kwargs):
|
||||
filename = str(filename)
|
||||
response = HttpResponse()
|
||||
response['X-Sendfile'] = filename
|
||||
|
||||
return response
|
@ -0,0 +1,85 @@
|
||||
from functools import lru_cache
|
||||
from importlib import import_module
|
||||
from pathlib import Path, PurePath
|
||||
from urllib.parse import quote
|
||||
import logging
|
||||
|
||||
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
|
||||
|
||||
|
||||
def sendfile(request, filename, mimetype="application/octet-stream", encoding=None):
|
||||
"""
|
||||
Create a response to send file using backend configured in ``SENDFILE_BACKEND``
|
||||
|
||||
``filename`` is the absolute path to the file to send.
|
||||
"""
|
||||
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)
|
||||
|
||||
response = _sendfile(request, filepath_obj, mimetype=mimetype)
|
||||
|
||||
response["Content-length"] = filepath_obj.stat().st_size
|
||||
response["Content-Type"] = mimetype
|
||||
|
||||
return response
|
Loading…
Reference in New Issue