diff --git a/ram/ram/__init__.py b/ram/ram/__init__.py index 62b94b0..30ab342 100644 --- a/ram/ram/__init__.py +++ b/ram/ram/__init__.py @@ -1,4 +1,4 @@ from ram.utils import git_suffix -__version__ = "0.19.6" +__version__ = "0.19.7" __version__ += git_suffix(__file__) diff --git a/ram/ram/urls.py b/ram/ram/urls.py index 8ff96d3..c436e4d 100644 --- a/ram/ram/urls.py +++ b/ram/ram/urls.py @@ -21,17 +21,22 @@ from django.conf.urls.static import static from django.contrib import admin from django.urls import include, path -from ram.views import UploadImage +from ram.views import UploadImage, DownloadFile from portal.views import Render404 handler404 = Render404.as_view() urlpatterns = [ path("", lambda r: redirect("portal/")), + path("admin/", admin.site.urls), path("tinymce/", include("tinymce.urls")), path("tinymce/upload_image", UploadImage.as_view(), name="upload_image"), + path( + "media/files/", + DownloadFile.as_view(), + name="download_file", + ), path("portal/", include("portal.urls")), - path("admin/", admin.site.urls), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) # Enable the "/dcc" routing only if the "driver" app is active @@ -55,6 +60,7 @@ if settings.DEBUG: if settings.REST_ENABLED: from django.views.generic import TemplateView from rest_framework.schemas import get_schema_view + urlpatterns += [ path( "swagger/", diff --git a/ram/ram/views.py b/ram/ram/views.py index 74fa63b..045a2a6 100644 --- a/ram/ram/views.py +++ b/ram/ram/views.py @@ -5,19 +5,25 @@ import posixpath from pathlib import Path from PIL import Image, UnidentifiedImageError -from django.views import View +from django.apps import apps from django.conf import settings from django.http import ( + Http404, HttpResponseBadRequest, HttpResponseForbidden, + FileResponse, JsonResponse, ) +from django.views import View from django.utils.text import slugify as slugify +from django.utils.encoding import smart_str from django.utils.decorators import method_decorator from django.views.decorators.csrf import csrf_exempt from rest_framework.pagination import LimitOffsetPagination +from ram.models import PrivateDocument + class CustomLimitOffsetPagination(LimitOffsetPagination): default_limit = 10 @@ -67,3 +73,37 @@ class UploadImage(View): ), } ) + + +class DownloadFile(View): + def get(self, request, filename, disposition="inline"): + # Clean up the filename to prevent directory traversal attacks + filename = os.path.basename(filename) + + # Find a document where the stored file name matches + # Find all models inheriting from PublishableFile + for model in apps.get_models(): + if issubclass(model, PrivateDocument) and not model._meta.abstract: + try: + doc = model.objects.get(file__endswith=filename) + if doc.private and not request.user.is_staff: + break + + file_path = doc.file.path + if not os.path.exists(file_path): + break + + response = FileResponse( + open(file_path, "rb"), as_attachment=True + ) + response["Content-Disposition"] = ( + '{}; filename="{}"'.format( + disposition, + smart_str(os.path.basename(file_path)) + ) + ) + return response + except model.DoesNotExist: + continue + + raise Http404("File not found")