Serve upload files directly from nginx

This commit is contained in:
Antoine Bartuccio 2025-04-06 12:08:15 +02:00
parent 91b30e7550
commit 67bc49fb21
10 changed files with 65 additions and 52 deletions

View File

@ -102,6 +102,7 @@ class OperationLogAdmin(admin.ModelAdmin):
@admin.register(QuickUploadImage) @admin.register(QuickUploadImage)
class QuickUploadImageAdmin(admin.ModelAdmin): class QuickUploadImageAdmin(admin.ModelAdmin):
list_display = ("uuid", "name", "uploader", "content_type", "date") list_display = ("uuid", "uploader", "created_at", "name")
search_fields = ("uuid", "name", "uploader") search_fields = ("uuid", "uploader", "name")
autocomplete_fields = ("uploader",) autocomplete_fields = ("uploader",)
readonly_fields = ("width", "height", "size")

View File

@ -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 import django.db.models.deletion
from django.conf import settings from django.conf import settings
@ -15,13 +15,24 @@ class Migration(migrations.Migration):
name="QuickUploadImage", name="QuickUploadImage",
fields=[ fields=[
( (
"uuid", "id",
models.CharField(max_length=36, primary_key=True, serialize=False), 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)), ("name", models.CharField(max_length=100)),
("image", models.ImageField(upload_to="upload")), ("image", models.ImageField(upload_to="upload/%Y/%m/%d")),
("content_type", models.CharField(max_length=50)), (
("date", models.DateTimeField(auto_now=True, verbose_name="date")), "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", "uploader",
models.ForeignKey( models.ForeignKey(

View File

@ -1110,12 +1110,10 @@ class QuickUploadImage(models.Model):
"""Images uploaded by user outside of the SithFile mechanism""" """Images uploaded by user outside of the SithFile mechanism"""
IMAGE_NAME_SIZE = 100 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) name = models.CharField(max_length=IMAGE_NAME_SIZE, blank=False)
image = models.ImageField(upload_to="upload") image = models.ImageField(upload_to="upload/%Y/%m/%d")
content_type = models.CharField(max_length=50, blank=False)
uploader = models.ForeignKey( uploader = models.ForeignKey(
"User", "User",
related_name="quick_uploads", related_name="quick_uploads",
@ -1123,13 +1121,16 @@ class QuickUploadImage(models.Model):
blank=True, blank=True,
on_delete=models.SET_NULL, 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: def __str__(self) -> str:
return f"{self.name}{Path(self.image.path).suffix}" return str(self.image.path)
def get_absolute_url(self): def get_absolute_url(self):
return reverse("core:uploaded_image", kwargs={"image_uuid": self.uuid}) return self.image.url
@classmethod @classmethod
def create_from_uploaded( def create_from_uploaded(
@ -1145,13 +1146,16 @@ class QuickUploadImage(models.Model):
identifier = str(uuid4()) identifier = str(uuid4())
name = Path(image.name).stem[: cls.IMAGE_NAME_SIZE - 1] name = Path(image.name).stem[: cls.IMAGE_NAME_SIZE - 1]
file = File(convert_image(image), name=f"{identifier}.webp") file = File(convert_image(image), name=f"{identifier}.webp")
image = Image.open(file)
return cls.objects.create( return cls.objects.create(
uuid=identifier, uuid=identifier,
name=name, name=name,
image=file, image=file,
content_type="image/webp",
uploader=uploader, uploader=uploader,
width=image.width,
height=image.height,
size=file.size,
) )

View File

@ -63,25 +63,10 @@ class UserProfileSchema(ModelSchema):
class UploadedFileSchema(ModelSchema): class UploadedFileSchema(ModelSchema):
class Meta: class Meta:
model = QuickUploadImage model = QuickUploadImage
fields = ["uuid", "name", "content_type"] fields = ["uuid", "name", "width", "height", "size"]
width: int
height: int
size: int
href: str 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 @staticmethod
def resolve_href(obj: QuickUploadImage) -> str: def resolve_href(obj: QuickUploadImage) -> str:
return obj.get_absolute_url() return obj.get_absolute_url()

View File

@ -26,13 +26,14 @@ const loadEasyMde = (textarea: HTMLTextAreaElement) => {
return; return;
} }
onSuccess(response.data.href); onSuccess(response.data.href);
// Workaround function to add ! and image name to uploaded image // Workaround function to add an image name to uploaded image
// Without this, you get [](url) instead of ![name](url) // Without this, you get ![](url) instead of ![name](url)
let cursor = easymde.codemirror.getCursor(); 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); easymde.codemirror.replaceSelection(response.data.name);
// Move cursor at the end of the url and add a new line // Move cursor at the end of the url and add a new line

View File

@ -1,5 +1,6 @@
from io import BytesIO from io import BytesIO
from itertools import cycle from itertools import cycle
from pathlib import Path
from typing import Callable from typing import Callable
from uuid import uuid4 from uuid import uuid4
@ -291,6 +292,15 @@ def test_apply_rights_recursively():
), ),
200, 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(), lambda: old_subscriber_user.make(),
SimpleUploadedFile( SimpleUploadedFile(
@ -329,4 +339,8 @@ def test_quick_upload_image(
if expected_status != 200: if expected_status != 200:
return 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]
)

View File

@ -30,7 +30,6 @@ from core.converters import (
FourDigitYearConverter, FourDigitYearConverter,
TwoDigitMonthConverter, TwoDigitMonthConverter,
) )
from core.models import QuickUploadImage
from core.views import ( from core.views import (
FileDeleteView, FileDeleteView,
FileEditPropView, FileEditPropView,
@ -214,16 +213,6 @@ urlpatterns = [
"file/<int:file_id>/moderate/", FileModerateView.as_view(), name="file_moderate" "file/<int:file_id>/moderate/", FileModerateView.as_view(), name="file_moderate"
), ),
path("file/<int:file_id>/download/", send_file, name="download"), path("file/<int:file_id>/download/", send_file, name="download"),
path(
"file/<str:image_uuid>/uploads/",
lambda request, image_uuid: send_file(
request=request,
file_id=image_uuid,
file_class=QuickUploadImage,
file_attr="image",
),
name="uploaded_image",
),
# Page views # Page views
path("page/", PageListView.as_view(), name="page_list"), path("page/", PageListView.as_view(), name="page_list"),
path("page/create/", PageCreateView.as_view(), name="page_new"), path("page/create/", PageCreateView.as_view(), name="page_new"),

View File

@ -39,7 +39,7 @@ from core.auth.mixins import (
CanViewMixin, CanViewMixin,
can_view, 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.mixins import AllowFragment
from core.views.widgets.ajax_select import ( from core.views.widgets.ajax_select import (
AutoCompleteSelectMultipleGroup, AutoCompleteSelectMultipleGroup,
@ -87,7 +87,7 @@ def send_raw_file(path: Path) -> HttpResponse:
def send_file( def send_file(
request: HttpRequest, request: HttpRequest,
file_id: int | str, file_id: int | str,
file_class: type[SithFile | QuickUploadImage] = SithFile, file_class: type[SithFile] = SithFile,
file_attr: str = "file", file_attr: str = "file",
) -> HttpResponse: ) -> HttpResponse:
"""Send a protected file, if the user can see it. """Send a protected file, if the user can see it.

View File

@ -224,7 +224,7 @@ server {
location /static/; location /static/;
root /repertoire/du/projet; root /repertoire/du/projet;
} }
location ~ ^/data/(products|com|club_logos)/ { location ~ ^/data/(products|com|club_logos|upload)/ {
root /repertoire/du/projet; root /repertoire/du/projet;
} }
location ~ ^/data/(SAS|profiles|users|.compressed|.thumbnails)/ { location ~ ^/data/(SAS|profiles|users|.compressed|.thumbnails)/ {

View File

@ -1572,6 +1572,14 @@ msgstr "Ceci n'est pas une miniature de dossier valide"
msgid "You must provide a file" msgid "You must provide a file"
msgstr "Vous devez fournir un fichier" msgstr "Vous devez fournir un fichier"
#: core/models.py
msgid "width"
msgstr "largeur"
#: core/models.py
msgid "height"
msgstr "hauteur"
#: core/models.py #: core/models.py
msgid "page unix name" msgid "page unix name"
msgstr "nom unix de la page" msgstr "nom unix de la page"