Implement sendfile.

This commit is contained in:
Tom Hacohen
2020-12-28 18:44:55 +02:00
parent c7f09d3fef
commit f0a8689712
15 changed files with 46 additions and 124 deletions

View File

@@ -7,7 +7,6 @@ from django.core.files.base import ContentFile
from django.db import transaction, IntegrityError
from django.db.models import Q, QuerySet
from fastapi import APIRouter, Depends, status, Request
from fastapi.responses import FileResponse
from django_etebase import models
from .authentication import get_authenticated_user
@@ -26,6 +25,7 @@ from .utils import (
PERMISSIONS_READWRITE,
)
from .dependencies import get_collection_queryset, get_item_queryset, get_collection
from .sendfile import sendfile
User = get_user_model()
collection_router = APIRouter(route_class=MsgpackRoute, responses=permission_responses)
@@ -582,4 +582,4 @@ def chunk_download(
chunk = get_object_or_404(collection.chunks, uid=chunk_uid)
filename = chunk.chunkFile.path
return FileResponse(filename, media_type="application/octet-stream")
return sendfile(filename)

View 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.

View 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.

View File

@@ -0,0 +1 @@
from .utils import sendfile # noqa

View 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)})

View 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)})

View 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)

View File

@@ -0,0 +1,6 @@
from fastapi import Response
def sendfile(filename, **kwargs):
filename = str(filename)
return Response(headers={"X-Sendfile": filename})

View 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