Implement sendfile.
This commit is contained in:
28
etebase_fastapi/sendfile/LICENSE
Normal file
28
etebase_fastapi/sendfile/LICENSE
Normal file
@@ -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.
|
||||
3
etebase_fastapi/sendfile/README.md
Normal file
3
etebase_fastapi/sendfile/README.md
Normal file
@@ -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.
|
||||
1
etebase_fastapi/sendfile/__init__.py
Normal file
1
etebase_fastapi/sendfile/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .utils import sendfile # noqa
|
||||
0
etebase_fastapi/sendfile/backends/__init__.py
Normal file
0
etebase_fastapi/sendfile/backends/__init__.py
Normal file
9
etebase_fastapi/sendfile/backends/mod_wsgi.py
Normal file
9
etebase_fastapi/sendfile/backends/mod_wsgi.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
from fastapi import Response
|
||||
|
||||
from ..utils import _convert_file_to_url
|
||||
|
||||
|
||||
def sendfile(filename, **kwargs):
|
||||
return Response(headers={"Location": _convert_file_to_url(filename)})
|
||||
9
etebase_fastapi/sendfile/backends/nginx.py
Normal file
9
etebase_fastapi/sendfile/backends/nginx.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
from fastapi import Response
|
||||
|
||||
from ..utils import _convert_file_to_url
|
||||
|
||||
|
||||
def sendfile(filename, **kwargs):
|
||||
return Response(headers={"X-Accel-Redirect": _convert_file_to_url(filename)})
|
||||
12
etebase_fastapi/sendfile/backends/simple.py
Normal file
12
etebase_fastapi/sendfile/backends/simple.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
|
||||
def sendfile(filename, mimetype, **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.
|
||||
"""
|
||||
|
||||
return FileResponse(filename, media_type=mimetype)
|
||||
6
etebase_fastapi/sendfile/backends/xsendfile.py
Normal file
6
etebase_fastapi/sendfile/backends/xsendfile.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from fastapi import Response
|
||||
|
||||
|
||||
def sendfile(filename, **kwargs):
|
||||
filename = str(filename)
|
||||
return Response(headers={"X-Sendfile": filename})
|
||||
86
etebase_fastapi/sendfile/utils.py
Normal file
86
etebase_fastapi/sendfile/utils.py
Normal file
@@ -0,0 +1,86 @@
|
||||
from functools import lru_cache
|
||||
from importlib import import_module
|
||||
from pathlib import Path, PurePath
|
||||
from urllib.parse import quote
|
||||
import logging
|
||||
|
||||
from fastapi import status
|
||||
from ..exceptions import HttpError
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
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 HttpError("generic", "{} wrt {} is impossible".format(filepath_abs, path_root), status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
return filepath_abs
|
||||
|
||||
|
||||
def sendfile(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 HttpError("does_not_exist", '"%s" does not exist' % filepath_obj, status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
response = _sendfile(filepath_obj, mimetype=mimetype)
|
||||
|
||||
response.headers["Content-Type"] = mimetype
|
||||
|
||||
return response
|
||||
Reference in New Issue
Block a user