ذخیرهسازی تصاویر با 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:
@@ -15,3 +15,15 @@ CORS_ORIGINS=["http://localhost:5173","http://127.0.0.1:5173"]
|
|||||||
# مشخصات ادمین پیشفرض که هنگام seed ساخته میشود
|
# مشخصات ادمین پیشفرض که هنگام seed ساخته میشود
|
||||||
ADMIN_USERNAME=admin
|
ADMIN_USERNAME=admin
|
||||||
ADMIN_PASSWORD=aramesh123
|
ADMIN_PASSWORD=aramesh123
|
||||||
|
|
||||||
|
# Object Storage (MinIO) برای آپلود تصاویر پنل مدیریت
|
||||||
|
S3_ENDPOINT=http://localhost:9000
|
||||||
|
S3_ACCESS_KEY=aramesh
|
||||||
|
S3_SECRET_KEY=change-me-minio-secret
|
||||||
|
S3_BUCKET=aramesh-media
|
||||||
|
|
||||||
|
# پیشوند عمومی مسیر تصاویر — پروکسی Reverse باید آن را به باکت برساند
|
||||||
|
MEDIA_PUBLIC_BASE=/media
|
||||||
|
|
||||||
|
# حداکثر حجم آپلود تصویر (مگابایت)
|
||||||
|
MEDIA_MAX_UPLOAD_MB=10
|
||||||
|
|||||||
+12
-3
@@ -1,14 +1,15 @@
|
|||||||
# بکاند گروه ساختمانی آرامش (Aramesh Building Group API)
|
# بکاند گروه ساختمانی آرامش (Aramesh Building Group API)
|
||||||
|
|
||||||
بکاند سایت گروه ساختمانی آرامش — ساختهشده با **FastAPI** + **PostgreSQL** + **SQLAlchemy 2** + **JWT**.
|
بکاند سایت گروه ساختمانی آرامش — ساختهشده با **FastAPI** + **PostgreSQL** + **SQLAlchemy 2** + **JWT** + **MinIO** (ذخیره تصاویر) + **Traefik** (مسیریابی).
|
||||||
|
|
||||||
## امکانات
|
## امکانات
|
||||||
|
|
||||||
- **API عمومی محتوا**: برند، خدمات، پروژهها، مقالات، نظرات مشتریان، سوالات متداول، تیم، تایملاین، آمار، چرا آرامش، و بخش قبل/بعد
|
- **API عمومی محتوا**: برند، منو، هیرو، داستان، تیتر بخشها، هیرو صفحات، خدمات، پروژهها، مقالات، نظرات مشتریان، سوالات متداول، تیم، تایملاین، آمار، چرا آرامش، قبل/بعد، مدلهای سرمایهگذاری، مراحل کار، دفاتر، گالری، فرم تماس، خبرنامه و فوتر — **تمام متنهای سایت در دیتابیس است**
|
||||||
- **فرم تماس**: ثبت پیام مشتریان از سایت (`POST /api/contact`)
|
- **فرم تماس**: ثبت پیام مشتریان از سایت (`POST /api/contact`)
|
||||||
- **خبرنامه**: عضویت و مدیریت مشترکین (`POST /api/newsletter`)
|
- **خبرنامه**: عضویت و مدیریت مشترکین (`POST /api/newsletter`)
|
||||||
- **پنل مدیریت**: ورود با نام کاربری/رمز عبور و دریافت توکن JWT، سپس CRUD کامل همه محتواها، مدیریت پیامهای تماس و مشترکین
|
- **پنل مدیریت**: ورود با نام کاربری/رمز عبور و دریافت توکن JWT، سپس CRUD کامل همه محتواها، مدیریت پیامهای تماس و مشترکین
|
||||||
- **Seed خودکار**: همه محتوای فعلی سایت با یک دستور در دیتابیس قرار میگیرد
|
- **آپلود تصویر**: `POST /api/admin/upload` — فایل در MinIO ذخیره و URL نسبی `/media/...` برمیگردد
|
||||||
|
- **Seed خودکار**: همه محتوای فعلی سایت با یک دستور در دیتابیس قرار میگیرد (فقط جدولهای خالی پر میشوند — ارتقا بدون از دست رفتن داده)
|
||||||
- مستندات تعاملی Swagger در مسیر `/docs`
|
- مستندات تعاملی Swagger در مسیر `/docs`
|
||||||
|
|
||||||
## راهاندازی
|
## راهاندازی
|
||||||
@@ -124,6 +125,14 @@ Authorization: Bearer <access_token>
|
|||||||
| DELETE | `/api/admin/messages/{id}` | حذف پیام |
|
| DELETE | `/api/admin/messages/{id}` | حذف پیام |
|
||||||
| GET | `/api/admin/subscribers` | مشترکین خبرنامه |
|
| GET | `/api/admin/subscribers` | مشترکین خبرنامه |
|
||||||
| DELETE | `/api/admin/subscribers/{id}` | حذف مشترک |
|
| DELETE | `/api/admin/subscribers/{id}` | حذف مشترک |
|
||||||
|
| POST | `/api/admin/upload` | آپلود تصویر (multipart، حداکثر ۱۰MB — jpg/png/webp/gif/avif) → `{"url": "/media/..."}` |
|
||||||
|
|
||||||
|
### تصاویر و MinIO
|
||||||
|
|
||||||
|
فایلهای آپلودشده در باکت `aramesh-media` روی MinIO ذخیره میشوند و با مسیر نسبی `/media/<سال>/<ماه>/<فایل>` برگردانده میشوند. Traefik مسیر `/media` را به MinIO میبرد، پس URLها مستقل از بکاند ذخیرهسازی هستند — بعداً برای مهاجرت به R2/S3 فقط تنظیمات را عوض کنید، نه محتوای دیتابیس را.
|
||||||
|
|
||||||
|
- کنسول MinIO: `http://localhost:9001` (کاربر/رمز از `MINIO_ROOT_USER` / `MINIO_ROOT_PASSWORD` در compose)
|
||||||
|
- تصاویر عمومی و قابل نمایشاند؛ آپلود فقط با توکن ادمین انجام میشود.
|
||||||
|
|
||||||
## اتصال به فرانتاند
|
## اتصال به فرانتاند
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
"""انتهای پنل مدیریت — همه مسیرها نیازمند توکن JWT معتبر هستند."""
|
"""انتهای پنل مدیریت — همه مسیرها نیازمند توکن 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.exc import IntegrityError
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from .. import models, schemas
|
from .. import models, schemas
|
||||||
from ..auth import create_access_token, get_current_admin, verify_password
|
from ..auth import create_access_token, get_current_admin, verify_password
|
||||||
|
from ..config import settings
|
||||||
from ..database import get_db
|
from ..database import get_db
|
||||||
from ..schemas import (
|
from ..schemas import (
|
||||||
ArticleIn,
|
ArticleIn,
|
||||||
@@ -158,6 +163,62 @@ register_crud("newsletter", models.NewsletterSectionContent, NewsletterOut, News
|
|||||||
register_crud("footer", models.FooterContent, FooterOut, FooterIn)
|
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])
|
@router.get("/messages", response_model=list[ContactMessageOut])
|
||||||
def list_messages(unread_only: bool = False, db: Session = Depends(get_db)):
|
def list_messages(unread_only: bool = False, db: Session = Depends(get_db)):
|
||||||
|
|||||||
@@ -18,5 +18,14 @@ class Settings(BaseSettings):
|
|||||||
admin_username: str = "admin"
|
admin_username: str = "admin"
|
||||||
admin_password: str = "aramesh123"
|
admin_password: str = "aramesh123"
|
||||||
|
|
||||||
|
# Object Storage (MinIO / هر S3 سازگار) برای آپلود تصاویر
|
||||||
|
s3_endpoint: str = "http://localhost:9000"
|
||||||
|
s3_access_key: str = "aramesh"
|
||||||
|
s3_secret_key: str = "aramesh-minio-secret"
|
||||||
|
s3_bucket: str = "aramesh-media"
|
||||||
|
# پیشوند عمومی مسیر تصاویر — nginx آن را به باکت پروکسی میکند
|
||||||
|
media_public_base: str = "/media"
|
||||||
|
media_max_upload_mb: int = 10
|
||||||
|
|
||||||
|
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
|
|||||||
@@ -7,3 +7,5 @@ pydantic-settings>=2.6.1
|
|||||||
PyJWT>=2.10.1
|
PyJWT>=2.10.1
|
||||||
bcrypt>=4.2.1
|
bcrypt>=4.2.1
|
||||||
email-validator>=2.2.0
|
email-validator>=2.2.0
|
||||||
|
boto3>=1.35.0
|
||||||
|
python-multipart>=0.0.18
|
||||||
|
|||||||
Reference in New Issue
Block a user