feat(media): سقف آپلود ۳۰ مگابایت — لبه ۴۰MB، بک‌اند ۳۵MB، پیش‌چک ۳۰MB + رمزگذاری روی ترد تا رویدادلوپ قفل نشود

This commit is contained in:
aramesh-site deploy
2026-09-30 10:54:36 +00:00
parent b070125f51
commit 679195b590
3 changed files with 12 additions and 11 deletions
+6 -4
View File
@@ -22,6 +22,7 @@ from pydantic import BaseModel
from sqlalchemy import func from sqlalchemy import func
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from starlette.concurrency import run_in_threadpool
from .. import models, schemas from .. import models, schemas
from ..auth import create_access_token, get_current_admin, get_user_from_token, hash_password, oauth2_scheme, verify_password from ..auth import create_access_token, get_current_admin, get_user_from_token, hash_password, oauth2_scheme, verify_password
@@ -530,8 +531,9 @@ async def upload_media(
detail=f"حجم فایل بیش از {settings.media_max_upload_mb} مگابایت است", detail=f"حجم فایل بیش از {settings.media_max_upload_mb} مگابایت است",
) )
# هر ورودی قابل‌رمزگشایی (تصویر رایج/خاص، متحرک، ویدیو) → WebP نرمال # هر ورودی قابل‌رمزگشایی (تصویر رایج/خاص، متحرک، ویدیو) → WebP نرمال — روی ترد،
data, width, height = _normalize_media(data) # تا رمزگذاری فایل حجیم رویداد‌لوپ و بقیهٔ درخواست‌ها را قفل نکند
data, width, height = await run_in_threadpool(_normalize_media, data)
# نام خوانا: نام ارسالی فرم، وگرنه نام اصلی فایل بدون پسوند # نام خوانا: نام ارسالی فرم، وگرنه نام اصلی فایل بدون پسوند
display_name = (name or "").strip() or (file.filename or "").rsplit(".", 1)[0] display_name = (name or "").strip() or (file.filename or "").rsplit(".", 1)[0]
@@ -595,8 +597,8 @@ async def import_image(
detail=f"حجم تصویر بیش از {settings.media_max_upload_mb} مگابایت است", detail=f"حجم تصویر بیش از {settings.media_max_upload_mb} مگابایت است",
) )
# هدر Content-Type دروغ می‌گوید — خود بدنه رمزگشایی و به WebP نرمال می‌شود # هدر Content-Type دروغ می‌گوید — خود بدنه رمزگشایی و به WebP نرمال می‌شود (روی ترد)
data, width, height = _normalize_media(data) data, width, height = await run_in_threadpool(_normalize_media, data)
# نام خوانا از مسیر نشانی — بدون پارامترهای کوئری # نام خوانا از مسیر نشانی — بدون پارامترهای کوئری
url_name = urlparse(payload.url).path.rsplit("/", 1)[-1].rsplit(".", 1)[0] url_name = urlparse(payload.url).path.rsplit("/", 1)[-1].rsplit(".", 1)[0]
+2 -2
View File
@@ -26,8 +26,8 @@ class Settings(BaseSettings):
s3_bucket: str = "aramesh-media" s3_bucket: str = "aramesh-media"
# پیشوند عمومی مسیر تصاویر — Traefik آن را به باکت پروکسی می‌کند # پیشوند عمومی مسیر تصاویر — Traefik آن را به باکت پروکسی می‌کند
media_public_base: str = "/media" media_public_base: str = "/media"
# رندرهای معماری حجیم‌اند — لبه (Traefik api-bodylimit) باید همیشه بزرگ‌تر از این باشد # رندرهای معماری تا ~۳۰ مگابایت — لبه (Traefik api-bodylimit = ۴۰MB) باید همیشه بزرگ‌تر باشد
media_max_upload_mb: int = 25 media_max_upload_mb: int = 35
# نشانی کامل سایت (مثل https://aramesh.example) — برای sitemap و آدرس مطلق OG # نشانی کامل سایت (مثل https://aramesh.example) — برای sitemap و آدرس مطلق OG
site_url: str = "" site_url: str = ""
+4 -5
View File
@@ -3,9 +3,8 @@ import { API_URL } from '../config'
const TOKEN_KEY = 'aramesh_admin_token' const TOKEN_KEY = 'aramesh_admin_token'
/** سقف عملی آپلود — CDN (WCDN) بدنه‌های بالای ~۱۹ مگابایت را قطع می‌کند؛ /** سقف آپلود — هم‌اندازهٔ سقف کاربر در بک‌اند (media_max_upload_mb = ۳۵ با احتساب سربار multipart) */
* بک‌اند ۲۵ و لبه ۳۰ اجازه می‌دهد تا با بالا بردن سقف CDN همین‌جا تنظیم شود */ const MAX_UPLOAD_BYTES = 30 * 1024 * 1024
const MAX_UPLOAD_BYTES = 18 * 1024 * 1024
/** رویداد تغییر وضعیت ورود — لایه‌های دیگر (مثل نوار ادمین) با آن همگام می‌مانند */ /** رویداد تغییر وضعیت ورود — لایه‌های دیگر (مثل نوار ادمین) با آن همگام می‌مانند */
export const AUTH_CHANGE_EVENT = 'aramesh:auth-change' export const AUTH_CHANGE_EVENT = 'aramesh:auth-change'
@@ -94,9 +93,9 @@ export const adminApi = {
authFetch<T[]>(`/api/admin/${resource}/reorder`, { method: 'POST', body: JSON.stringify({ ids }) }), authFetch<T[]>(`/api/admin/${resource}/reorder`, { method: 'POST', body: JSON.stringify({ ids }) }),
upload: async (file: File, name?: string): Promise<{ url: string; key: string }> => { upload: async (file: File, name?: string): Promise<{ url: string; key: string }> => {
// پیش‌چک سمت کاربر — بدنه‌های بزرگ‌تر از سقف CDN بی‌پاسخ قطع می‌شوند و ۴۱۳ لبه به‌صورت HTML می‌رسد // پیش‌چک سمت کاربر — بازخورد فوری به‌جای ۴۱۳ لبه/CDN که به‌صورت HTML می‌رسد
if (file.size > MAX_UPLOAD_BYTES) { if (file.size > MAX_UPLOAD_BYTES) {
throw new HttpError(413, 'حجم فایل بیش از ۱۸ مگابایت است — فایل را کوچک‌تر کنید') throw new HttpError(413, 'حجم فایل بیش از ۳۰ مگابایت است — فایل را کوچک‌تر کنید')
} }
const fd = new FormData() const fd = new FormData()
fd.append('file', file) fd.append('file', file)