From ff6f409c5a94ff121698ac5b6c622b7cf67f1544 Mon Sep 17 00:00:00 2001 From: benya Date: Sat, 7 Mar 2026 23:49:14 +0300 Subject: [PATCH] feat: improve media delivery and web upload pipeline - make minio bucket downloadable for direct media links - switch object keys to random uuid-based names - add client-side image compression before upload --- app/media/service.py | 21 +++++------ docker-compose.yml | 3 +- web/src/components/MessageComposer.tsx | 52 +++++++++++++++++++++++++- 3 files changed, 63 insertions(+), 13 deletions(-) diff --git a/app/media/service.py b/app/media/service.py index 97014ac..70eafc4 100644 --- a/app/media/service.py +++ b/app/media/service.py @@ -1,4 +1,4 @@ -import re +import mimetypes from urllib.parse import quote from uuid import uuid4 @@ -27,14 +27,13 @@ ALLOWED_MIME_TYPES = { "text/plain", } -_SAFE_NAME_RE = re.compile(r"[^a-zA-Z0-9._-]+") - - -def _sanitize_filename(file_name: str) -> str: - sanitized = _SAFE_NAME_RE.sub("_", file_name).strip("._") - if not sanitized: - sanitized = "file.bin" - return sanitized[:120] +def _extension_from_mime(file_type: str) -> str: + ext = mimetypes.guess_extension(file_type) + if not ext: + return ".bin" + if ext == ".jpe": + return ".jpg" + return ext def _build_file_url(bucket: str, object_key: str) -> str: @@ -71,8 +70,8 @@ def _get_s3_client(endpoint_url: str): async def generate_upload_url(payload: UploadUrlRequest) -> UploadUrlResponse: _validate_media(payload.file_type, payload.file_size) - file_name = _sanitize_filename(payload.file_name) - object_key = f"uploads/{uuid4()}-{file_name}" + extension = _extension_from_mime(payload.file_type) + object_key = f"uploads/{uuid4().hex}{extension}" bucket = settings.s3_bucket_name try: diff --git a/docker-compose.yml b/docker-compose.yml index 2642f75..8389fb4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -92,7 +92,8 @@ services: - -ec - > mc alias set local http://minio:9000 ${MINIO_ROOT_USER:-minioadmin} ${MINIO_ROOT_PASSWORD:-minioadmin}; - mc mb --ignore-existing local/${S3_BUCKET_NAME:-messenger-media} + mc mb --ignore-existing local/${S3_BUCKET_NAME:-messenger-media}; + mc anonymous set download local/${S3_BUCKET_NAME:-messenger-media} restart: "no" backend: diff --git a/web/src/components/MessageComposer.tsx b/web/src/components/MessageComposer.tsx index bfc1458..5894953 100644 --- a/web/src/components/MessageComposer.tsx +++ b/web/src/components/MessageComposer.tsx @@ -84,6 +84,55 @@ export function MessageComposer() { return "file"; } + function loadImageFromFile(file: File): Promise { + return new Promise((resolve, reject) => { + const image = new Image(); + const url = URL.createObjectURL(file); + image.onload = () => { + URL.revokeObjectURL(url); + resolve(image); + }; + image.onerror = () => { + URL.revokeObjectURL(url); + reject(new Error("Failed to load image")); + }; + image.src = url; + }); + } + + async function compressImageForWeb(file: File): Promise { + const image = await loadImageFromFile(file); + const maxSide = 1920; + const ratio = Math.min(1, maxSide / Math.max(image.width, image.height)); + const width = Math.max(1, Math.round(image.width * ratio)); + const height = Math.max(1, Math.round(image.height * ratio)); + + const canvas = document.createElement("canvas"); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext("2d"); + if (!ctx) { + return file; + } + ctx.drawImage(image, 0, 0, width, height); + + const blob = await new Promise((resolve) => { + canvas.toBlob((result) => resolve(result), "image/jpeg", 0.82); + }); + if (!blob || blob.size >= file.size) { + return file; + } + const baseName = file.name.replace(/\.[^/.]+$/, ""); + return new File([blob], `${baseName || "image"}-web.jpg`, { type: "image/jpeg" }); + } + + async function prepareFileForUpload(file: File, fileType: "file" | "image" | "video" | "audio"): Promise { + if (fileType === "image") { + return compressImageForWeb(file); + } + return file; + } + async function startRecord() { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); @@ -129,7 +178,8 @@ export function MessageComposer() { if (!selectedFile) { return; } - await handleUpload(selectedFile, selectedType); + const uploadFile = await prepareFileForUpload(selectedFile, selectedType); + await handleUpload(uploadFile, selectedType); if (previewUrl) { URL.revokeObjectURL(previewUrl); }