From f0a8689712601ca2ac5b98b5c3c4aad4f6d58808 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Mon, 28 Dec 2020 18:44:55 +0200 Subject: [PATCH] Implement sendfile. --- .../sendfile/backends/development.py | 17 ------ django_etebase/sendfile/backends/mod_wsgi.py | 17 ------ django_etebase/sendfile/backends/nginx.py | 12 ---- django_etebase/sendfile/backends/simple.py | 60 ------------------- django_etebase/sendfile/backends/xsendfile.py | 9 --- etebase_fastapi/collection.py | 4 +- .../sendfile/LICENSE | 0 .../sendfile/README.md | 0 .../sendfile/__init__.py | 0 .../sendfile/backends/__init__.py | 0 etebase_fastapi/sendfile/backends/mod_wsgi.py | 9 +++ etebase_fastapi/sendfile/backends/nginx.py | 9 +++ etebase_fastapi/sendfile/backends/simple.py | 12 ++++ .../sendfile/backends/xsendfile.py | 6 ++ .../sendfile/utils.py | 15 ++--- 15 files changed, 46 insertions(+), 124 deletions(-) delete mode 100644 django_etebase/sendfile/backends/development.py delete mode 100644 django_etebase/sendfile/backends/mod_wsgi.py delete mode 100644 django_etebase/sendfile/backends/nginx.py delete mode 100644 django_etebase/sendfile/backends/simple.py delete mode 100644 django_etebase/sendfile/backends/xsendfile.py rename {django_etebase => etebase_fastapi}/sendfile/LICENSE (100%) rename {django_etebase => etebase_fastapi}/sendfile/README.md (100%) rename {django_etebase => etebase_fastapi}/sendfile/__init__.py (100%) rename {django_etebase => etebase_fastapi}/sendfile/backends/__init__.py (100%) create mode 100644 etebase_fastapi/sendfile/backends/mod_wsgi.py create mode 100644 etebase_fastapi/sendfile/backends/nginx.py create mode 100644 etebase_fastapi/sendfile/backends/simple.py create mode 100644 etebase_fastapi/sendfile/backends/xsendfile.py rename {django_etebase => etebase_fastapi}/sendfile/utils.py (81%) diff --git a/django_etebase/sendfile/backends/development.py b/django_etebase/sendfile/backends/development.py deleted file mode 100644 index d321932..0000000 --- a/django_etebase/sendfile/backends/development.py +++ /dev/null @@ -1,17 +0,0 @@ -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) diff --git a/django_etebase/sendfile/backends/mod_wsgi.py b/django_etebase/sendfile/backends/mod_wsgi.py deleted file mode 100644 index 07ba3f1..0000000 --- a/django_etebase/sendfile/backends/mod_wsgi.py +++ /dev/null @@ -1,17 +0,0 @@ -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 diff --git a/django_etebase/sendfile/backends/nginx.py b/django_etebase/sendfile/backends/nginx.py deleted file mode 100644 index 8764309..0000000 --- a/django_etebase/sendfile/backends/nginx.py +++ /dev/null @@ -1,12 +0,0 @@ -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 diff --git a/django_etebase/sendfile/backends/simple.py b/django_etebase/sendfile/backends/simple.py deleted file mode 100644 index 0549b20..0000000 --- a/django_etebase/sendfile/backends/simple.py +++ /dev/null @@ -1,60 +0,0 @@ -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 diff --git a/django_etebase/sendfile/backends/xsendfile.py b/django_etebase/sendfile/backends/xsendfile.py deleted file mode 100644 index 74993ee..0000000 --- a/django_etebase/sendfile/backends/xsendfile.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.http import HttpResponse - - -def sendfile(request, filename, **kwargs): - filename = str(filename) - response = HttpResponse() - response['X-Sendfile'] = filename - - return response diff --git a/etebase_fastapi/collection.py b/etebase_fastapi/collection.py index c5b0801..3b672cc 100644 --- a/etebase_fastapi/collection.py +++ b/etebase_fastapi/collection.py @@ -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) diff --git a/django_etebase/sendfile/LICENSE b/etebase_fastapi/sendfile/LICENSE similarity index 100% rename from django_etebase/sendfile/LICENSE rename to etebase_fastapi/sendfile/LICENSE diff --git a/django_etebase/sendfile/README.md b/etebase_fastapi/sendfile/README.md similarity index 100% rename from django_etebase/sendfile/README.md rename to etebase_fastapi/sendfile/README.md diff --git a/django_etebase/sendfile/__init__.py b/etebase_fastapi/sendfile/__init__.py similarity index 100% rename from django_etebase/sendfile/__init__.py rename to etebase_fastapi/sendfile/__init__.py diff --git a/django_etebase/sendfile/backends/__init__.py b/etebase_fastapi/sendfile/backends/__init__.py similarity index 100% rename from django_etebase/sendfile/backends/__init__.py rename to etebase_fastapi/sendfile/backends/__init__.py diff --git a/etebase_fastapi/sendfile/backends/mod_wsgi.py b/etebase_fastapi/sendfile/backends/mod_wsgi.py new file mode 100644 index 0000000..b8fc6c0 --- /dev/null +++ b/etebase_fastapi/sendfile/backends/mod_wsgi.py @@ -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)}) diff --git a/etebase_fastapi/sendfile/backends/nginx.py b/etebase_fastapi/sendfile/backends/nginx.py new file mode 100644 index 0000000..b22e0d0 --- /dev/null +++ b/etebase_fastapi/sendfile/backends/nginx.py @@ -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)}) diff --git a/etebase_fastapi/sendfile/backends/simple.py b/etebase_fastapi/sendfile/backends/simple.py new file mode 100644 index 0000000..f3a3548 --- /dev/null +++ b/etebase_fastapi/sendfile/backends/simple.py @@ -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) diff --git a/etebase_fastapi/sendfile/backends/xsendfile.py b/etebase_fastapi/sendfile/backends/xsendfile.py new file mode 100644 index 0000000..530f6a1 --- /dev/null +++ b/etebase_fastapi/sendfile/backends/xsendfile.py @@ -0,0 +1,6 @@ +from fastapi import Response + + +def sendfile(filename, **kwargs): + filename = str(filename) + return Response(headers={"X-Sendfile": filename}) diff --git a/django_etebase/sendfile/utils.py b/etebase_fastapi/sendfile/utils.py similarity index 81% rename from django_etebase/sendfile/utils.py rename to etebase_fastapi/sendfile/utils.py index 97c06d7..7c8b1f2 100644 --- a/django_etebase/sendfile/utils.py +++ b/etebase_fastapi/sendfile/utils.py @@ -4,9 +4,11 @@ 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 -from django.http import Http404 logger = logging.getLogger(__name__) @@ -54,12 +56,12 @@ def _sanitize_path(filepath): try: filepath_abs.relative_to(path_root) except ValueError: - raise Http404("{} wrt {} is impossible".format(filepath_abs, path_root)) + raise HttpError("generic", "{} wrt {} is impossible".format(filepath_abs, path_root), status_code=status.HTTP_404_NOT_FOUND) return filepath_abs -def sendfile(request, filename, mimetype="application/octet-stream", encoding=None): +def sendfile(filename, mimetype="application/octet-stream", encoding=None): """ Create a response to send file using backend configured in ``SENDFILE_BACKEND`` @@ -75,11 +77,10 @@ def sendfile(request, filename, mimetype="application/octet-stream", encoding=No _sendfile = _get_sendfile() if not filepath_obj.exists(): - raise Http404('"%s" does not exist' % filepath_obj) + raise HttpError("does_not_exist", '"%s" does not exist' % filepath_obj, status_code=status.HTTP_404_NOT_FOUND) - response = _sendfile(request, filepath_obj, mimetype=mimetype) + response = _sendfile(filepath_obj, mimetype=mimetype) - response["Content-length"] = filepath_obj.stat().st_size - response["Content-Type"] = mimetype + response.headers["Content-Type"] = mimetype return response