From 67bc49fb2169de01c367f610fecf5cdfd0d084c7 Mon Sep 17 00:00:00 2001 From: Sli Date: Sun, 6 Apr 2025 12:08:15 +0200 Subject: [PATCH] Serve upload files directly from nginx --- core/admin.py | 5 ++-- core/migrations/0045_quickuploadimage.py | 23 ++++++++++++++----- core/models.py | 20 +++++++++------- core/schemas.py | 17 +------------- .../bundled/core/components/easymde-index.ts | 11 +++++---- core/tests/test_files.py | 16 ++++++++++++- core/urls.py | 11 --------- core/views/files.py | 4 ++-- docs/tutorial/install-advanced.md | 2 +- locale/fr/LC_MESSAGES/django.po | 8 +++++++ 10 files changed, 65 insertions(+), 52 deletions(-) diff --git a/core/admin.py b/core/admin.py index 894f6a55..eff77817 100644 --- a/core/admin.py +++ b/core/admin.py @@ -102,6 +102,7 @@ class OperationLogAdmin(admin.ModelAdmin): @admin.register(QuickUploadImage) class QuickUploadImageAdmin(admin.ModelAdmin): - list_display = ("uuid", "name", "uploader", "content_type", "date") - search_fields = ("uuid", "name", "uploader") + list_display = ("uuid", "uploader", "created_at", "name") + search_fields = ("uuid", "uploader", "name") autocomplete_fields = ("uploader",) + readonly_fields = ("width", "height", "size") diff --git a/core/migrations/0045_quickuploadimage.py b/core/migrations/0045_quickuploadimage.py index ee6a6a7b..83edcc65 100644 --- a/core/migrations/0045_quickuploadimage.py +++ b/core/migrations/0045_quickuploadimage.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.20 on 2025-04-05 16:28 +# Generated by Django 4.2.20 on 2025-04-06 09:55 import django.db.models.deletion from django.conf import settings @@ -15,13 +15,24 @@ class Migration(migrations.Migration): name="QuickUploadImage", fields=[ ( - "uuid", - models.CharField(max_length=36, primary_key=True, serialize=False), + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), ), + ("uuid", models.UUIDField(db_index=True, unique=True)), ("name", models.CharField(max_length=100)), - ("image", models.ImageField(upload_to="upload")), - ("content_type", models.CharField(max_length=50)), - ("date", models.DateTimeField(auto_now=True, verbose_name="date")), + ("image", models.ImageField(upload_to="upload/%Y/%m/%d")), + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="created at"), + ), + ("width", models.PositiveIntegerField(verbose_name="width")), + ("height", models.PositiveIntegerField(verbose_name="height")), + ("size", models.PositiveIntegerField(verbose_name="size")), ( "uploader", models.ForeignKey( diff --git a/core/models.py b/core/models.py index 883d21f4..7169435d 100644 --- a/core/models.py +++ b/core/models.py @@ -1110,12 +1110,10 @@ class QuickUploadImage(models.Model): """Images uploaded by user outside of the SithFile mechanism""" IMAGE_NAME_SIZE = 100 - UUID_4_SIZE = 36 - uuid = models.CharField(max_length=UUID_4_SIZE, blank=False, primary_key=True) + uuid = models.UUIDField(unique=True, db_index=True) name = models.CharField(max_length=IMAGE_NAME_SIZE, blank=False) - image = models.ImageField(upload_to="upload") - content_type = models.CharField(max_length=50, blank=False) + image = models.ImageField(upload_to="upload/%Y/%m/%d") uploader = models.ForeignKey( "User", related_name="quick_uploads", @@ -1123,13 +1121,16 @@ class QuickUploadImage(models.Model): blank=True, on_delete=models.SET_NULL, ) - date = models.DateTimeField(_("date"), auto_now=True) + created_at = models.DateTimeField(_("created at"), auto_now_add=True) + width = models.PositiveIntegerField(_("width")) + height = models.PositiveIntegerField(_("height")) + size = models.PositiveIntegerField(_("size")) def __str__(self) -> str: - return f"{self.name}{Path(self.image.path).suffix}" + return str(self.image.path) def get_absolute_url(self): - return reverse("core:uploaded_image", kwargs={"image_uuid": self.uuid}) + return self.image.url @classmethod def create_from_uploaded( @@ -1145,13 +1146,16 @@ class QuickUploadImage(models.Model): identifier = str(uuid4()) name = Path(image.name).stem[: cls.IMAGE_NAME_SIZE - 1] file = File(convert_image(image), name=f"{identifier}.webp") + image = Image.open(file) return cls.objects.create( uuid=identifier, name=name, image=file, - content_type="image/webp", uploader=uploader, + width=image.width, + height=image.height, + size=file.size, ) diff --git a/core/schemas.py b/core/schemas.py index f532d676..b5f5991f 100644 --- a/core/schemas.py +++ b/core/schemas.py @@ -63,25 +63,10 @@ class UserProfileSchema(ModelSchema): class UploadedFileSchema(ModelSchema): class Meta: model = QuickUploadImage - fields = ["uuid", "name", "content_type"] + fields = ["uuid", "name", "width", "height", "size"] - width: int - height: int - size: int href: str - @staticmethod - def resolve_width(obj: QuickUploadImage): - return obj.image.width - - @staticmethod - def resolve_height(obj: QuickUploadImage): - return obj.image.height - - @staticmethod - def resolve_size(obj: QuickUploadImage): - return obj.image.size - @staticmethod def resolve_href(obj: QuickUploadImage) -> str: return obj.get_absolute_url() diff --git a/core/static/bundled/core/components/easymde-index.ts b/core/static/bundled/core/components/easymde-index.ts index 10ad06a5..39539409 100644 --- a/core/static/bundled/core/components/easymde-index.ts +++ b/core/static/bundled/core/components/easymde-index.ts @@ -26,13 +26,14 @@ const loadEasyMde = (textarea: HTMLTextAreaElement) => { return; } onSuccess(response.data.href); - // Workaround function to add ! and image name to uploaded image - // Without this, you get [](url) instead of ![name](url) + // Workaround function to add an image name to uploaded image + // Without this, you get ![](url) instead of ![name](url) let cursor = easymde.codemirror.getCursor(); - easymde.codemirror.setSelection({ line: cursor.line, ch: cursor.ch - 1 }); - easymde.codemirror.replaceSelection("!"); - easymde.codemirror.setSelection({ line: cursor.line, ch: cursor.ch + 1 }); + easymde.codemirror.setSelection({ + line: cursor.line, + ch: cursor.ch - response.data.href.length - 3, + }); easymde.codemirror.replaceSelection(response.data.name); // Move cursor at the end of the url and add a new line diff --git a/core/tests/test_files.py b/core/tests/test_files.py index 37557db1..6c4c30e8 100644 --- a/core/tests/test_files.py +++ b/core/tests/test_files.py @@ -1,5 +1,6 @@ from io import BytesIO from itertools import cycle +from pathlib import Path from typing import Callable from uuid import uuid4 @@ -291,6 +292,15 @@ def test_apply_rights_recursively(): ), 200, ), + ( + lambda: old_subscriber_user.make(), + SimpleUploadedFile( + "ttesttesttesttesttesttesttesttesttesttesttesttesttesttesttestesttesttesttesttesttesttesttesttesttesttesttest.jpg", + content=RED_PIXEL_PNG, + content_type="image/jpg", + ), + 200, + ), # very long file name ( lambda: old_subscriber_user.make(), SimpleUploadedFile( @@ -329,4 +339,8 @@ def test_quick_upload_image( if expected_status != 200: return - assert QuickUploadImage.objects.filter(pk=resp.json()["uuid"]).exists() + parsed = resp.json() + assert QuickUploadImage.objects.filter(uuid=parsed["uuid"]).exists() + assert ( + parsed["name"] == Path(file.name).stem[: QuickUploadImage.IMAGE_NAME_SIZE - 1] + ) diff --git a/core/urls.py b/core/urls.py index 10810458..23fa9f11 100644 --- a/core/urls.py +++ b/core/urls.py @@ -30,7 +30,6 @@ from core.converters import ( FourDigitYearConverter, TwoDigitMonthConverter, ) -from core.models import QuickUploadImage from core.views import ( FileDeleteView, FileEditPropView, @@ -214,16 +213,6 @@ urlpatterns = [ "file//moderate/", FileModerateView.as_view(), name="file_moderate" ), path("file//download/", send_file, name="download"), - path( - "file//uploads/", - lambda request, image_uuid: send_file( - request=request, - file_id=image_uuid, - file_class=QuickUploadImage, - file_attr="image", - ), - name="uploaded_image", - ), # Page views path("page/", PageListView.as_view(), name="page_list"), path("page/create/", PageCreateView.as_view(), name="page_new"), diff --git a/core/views/files.py b/core/views/files.py index 5848666f..cd9103d9 100644 --- a/core/views/files.py +++ b/core/views/files.py @@ -39,7 +39,7 @@ from core.auth.mixins import ( CanViewMixin, can_view, ) -from core.models import Notification, QuickUploadImage, SithFile, User +from core.models import Notification, SithFile, User from core.views.mixins import AllowFragment from core.views.widgets.ajax_select import ( AutoCompleteSelectMultipleGroup, @@ -87,7 +87,7 @@ def send_raw_file(path: Path) -> HttpResponse: def send_file( request: HttpRequest, file_id: int | str, - file_class: type[SithFile | QuickUploadImage] = SithFile, + file_class: type[SithFile] = SithFile, file_attr: str = "file", ) -> HttpResponse: """Send a protected file, if the user can see it. diff --git a/docs/tutorial/install-advanced.md b/docs/tutorial/install-advanced.md index 2da2fc42..2998298f 100644 --- a/docs/tutorial/install-advanced.md +++ b/docs/tutorial/install-advanced.md @@ -224,7 +224,7 @@ server { location /static/; root /repertoire/du/projet; } - location ~ ^/data/(products|com|club_logos)/ { + location ~ ^/data/(products|com|club_logos|upload)/ { root /repertoire/du/projet; } location ~ ^/data/(SAS|profiles|users|.compressed|.thumbnails)/ { diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 249eee49..11100e5d 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -1572,6 +1572,14 @@ msgstr "Ceci n'est pas une miniature de dossier valide" msgid "You must provide a file" msgstr "Vous devez fournir un fichier" +#: core/models.py +msgid "width" +msgstr "largeur" + +#: core/models.py +msgid "height" +msgstr "hauteur" + #: core/models.py msgid "page unix name" msgstr "nom unix de la page"