ذخیرهسازی تصاویر با MinIO (S3) و انتهاینقطه آپلود ادمین
- POST /api/admin/upload با احراز هویت JWT: ذخیره فایل در باکت S3 و برگرداندن URL نسبی /media/<سال>/<ماه>/<uuid>.<پسوند> - فرمتهای مجاز: jpg، png، webp، gif، avif — حداکثر ۱۰ مگابایت (قابل تنظیم) - افزودن boto3 و python-multipart به وابستگیها - تنظیمات جدید: S3_ENDPOINT، S3_ACCESS_KEY، S3_SECRET_KEY، S3_BUCKET، MEDIA_PUBLIC_BASE، MEDIA_MAX_UPLOAD_MB (قابل تعریف در .env) - URL نسبی برمیگردد تا بکاند ذخیرهسازی (MinIO/R2/هر S3) بدون تغییر محتوای دیتابیس قابل تعویض باشد - بهروزرسانی .env.example و README (بخش تصاویر و MinIO)
This commit is contained in:
@@ -1,11 +1,16 @@
|
||||
"""انتهای پنل مدیریت — همه مسیرها نیازمند توکن JWT معتبر هستند."""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import boto3
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .. import models, schemas
|
||||
from ..auth import create_access_token, get_current_admin, verify_password
|
||||
from ..config import settings
|
||||
from ..database import get_db
|
||||
from ..schemas import (
|
||||
ArticleIn,
|
||||
@@ -158,6 +163,62 @@ register_crud("newsletter", models.NewsletterSectionContent, NewsletterOut, News
|
||||
register_crud("footer", models.FooterContent, FooterOut, FooterIn)
|
||||
|
||||
|
||||
# آپلود تصویر به Object Storage (MinIO) — URL نسبی برمیگرداند تا بکاند ذخیرهسازی قابل تعویض باشد
|
||||
ALLOWED_IMAGE_TYPES = {
|
||||
"image/jpeg": "jpg",
|
||||
"image/png": "png",
|
||||
"image/webp": "webp",
|
||||
"image/gif": "gif",
|
||||
"image/avif": "avif",
|
||||
}
|
||||
|
||||
|
||||
@router.post("/upload")
|
||||
async def upload_media(
|
||||
file: UploadFile = File(...),
|
||||
_admin: models.AdminUser = Depends(get_current_admin),
|
||||
):
|
||||
content_type = (file.content_type or "").lower()
|
||||
ext = ALLOWED_IMAGE_TYPES.get(content_type)
|
||||
if ext is None:
|
||||
raise HTTPException(
|
||||
status_code=415,
|
||||
detail="فرمت فایل پشتیبانی نمیشود (مجاز: jpg، png، webp، gif، avif)",
|
||||
)
|
||||
|
||||
max_bytes = settings.media_max_upload_mb * 1024 * 1024
|
||||
data = await file.read()
|
||||
if len(data) == 0:
|
||||
raise HTTPException(status_code=400, detail="فایل خالی است")
|
||||
if len(data) > max_bytes:
|
||||
raise HTTPException(
|
||||
status_code=413,
|
||||
detail=f"حجم فایل بیش از {settings.media_max_upload_mb} مگابایت است",
|
||||
)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
key = f"{now:%Y/%m}/{uuid.uuid4().hex}.{ext}"
|
||||
|
||||
s3 = boto3.client(
|
||||
"s3",
|
||||
endpoint_url=settings.s3_endpoint,
|
||||
aws_access_key_id=settings.s3_access_key,
|
||||
aws_secret_access_key=settings.s3_secret_key,
|
||||
region_name="us-east-1",
|
||||
)
|
||||
try:
|
||||
s3.put_object(Bucket=settings.s3_bucket, Key=key, Body=data, ContentType=content_type)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=502, detail="ذخیره فایل در Object Storage ناموفق بود")
|
||||
|
||||
return {
|
||||
"url": f"{settings.media_public_base}/{key}",
|
||||
"key": key,
|
||||
"size": len(data),
|
||||
"content_type": content_type,
|
||||
}
|
||||
|
||||
|
||||
# پیامهای تماس
|
||||
@router.get("/messages", response_model=list[ContactMessageOut])
|
||||
def list_messages(unread_only: bool = False, db: Session = Depends(get_db)):
|
||||
|
||||
Reference in New Issue
Block a user