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