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

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
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(

View File

@ -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,
)

View File

@ -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()

View File

@ -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

View File

@ -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]
)

View File

@ -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/<int:file_id>/moderate/", FileModerateView.as_view(), name="file_moderate"
),
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
path("page/", PageListView.as_view(), name="page_list"),
path("page/create/", PageCreateView.as_view(), name="page_new"),

View File

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

View File

@ -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)/ {

View File

@ -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"