Files
news_all_spo/api/route/admin_media.py
2026-05-15 20:12:44 +05:00

187 lines
6.0 KiB
Python

import os
import io
import uuid
from datetime import datetime
from fastapi import APIRouter, Depends, UploadFile, File, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from minio import Minio
from minio.error import S3Error
from slugify import slugify
from bd.database import get_db
from bd.models import Media, Article
from route.deps import require_admin
router = APIRouter()
MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT", "minio:9000")
MINIO_ACCESS_KEY = os.getenv("MINIO_ACCESS_KEY", "minioadmin")
MINIO_SECRET_KEY = os.getenv("MINIO_SECRET_KEY", "minioadmin")
MINIO_BUCKET = os.getenv("MINIO_BUCKET", "news-media")
MINIO_PUBLIC_URL = os.getenv("MINIO_PUBLIC_URL", "http://localhost:9000")
ALLOWED_IMAGE = {"image/jpeg", "image/png", "image/gif", "image/webp", "image/svg+xml"}
ALLOWED_VIDEO = {"video/mp4", "video/webm", "video/ogg", "video/quicktime"}
MAX_SIZE_BYTES = 100 * 1024 * 1024 # 100 MB
def get_minio() -> Minio:
return Minio(MINIO_ENDPOINT, access_key=MINIO_ACCESS_KEY, secret_key=MINIO_SECRET_KEY, secure=False)
def ensure_bucket(client: Minio):
try:
if not client.bucket_exists(MINIO_BUCKET):
client.make_bucket(MINIO_BUCKET)
policy = f"""{{
"Version":"2012-10-17",
"Statement":[{{
"Effect":"Allow",
"Principal":"*",
"Action":["s3:GetObject"],
"Resource":["arn:aws:s3:::{MINIO_BUCKET}/*"]
}}]
}}"""
client.set_bucket_policy(MINIO_BUCKET, policy)
except S3Error:
pass
@router.post("/upload")
async def upload_media(
file: UploadFile = File(...),
article_id: str | None = Query(None),
db: AsyncSession = Depends(get_db),
_: str = Depends(require_admin),
):
content_type = file.content_type or ""
if content_type not in ALLOWED_IMAGE and content_type not in ALLOWED_VIDEO:
raise HTTPException(status_code=400, detail=f"Тип файла не поддерживается: {content_type}")
data = await file.read()
if len(data) > MAX_SIZE_BYTES:
raise HTTPException(status_code=400, detail="Файл слишком большой (макс. 100 МБ)")
media_type = "image" if content_type in ALLOWED_IMAGE else "video"
ext = (file.filename or "file").rsplit(".", 1)[-1].lower()
# Определяем путь в MinIO
art_uuid = None
if article_id:
try:
art_uuid = uuid.UUID(article_id)
# Проверяем существование статьи
art = (await db.execute(select(Article).where(Article.id == art_uuid))).scalar_one_or_none()
if not art:
art_uuid = None
except ValueError:
art_uuid = None
if art_uuid:
folder = "images" if media_type == "image" else "videos"
art_slug = slugify(art.title) if art and art.title else str(art_uuid)[:8]
art_dir = f"{art_slug}-{str(art_uuid)[:8]}"
base_name = f"{art_slug}-{uuid.uuid4().hex[:6]}.{ext}"
object_name = f"articles/{art_dir}/{folder}/{base_name}"
else:
object_name = f"{media_type}s/{uuid.uuid4().hex}.{ext}"
client = get_minio()
ensure_bucket(client)
client.put_object(
MINIO_BUCKET,
object_name,
io.BytesIO(data),
length=len(data),
content_type=content_type,
)
public_url = f"{MINIO_PUBLIC_URL}/{MINIO_BUCKET}/{object_name}"
media = Media(
filename=object_name.split("/")[-1],
url=public_url,
media_type=media_type,
size_bytes=len(data),
article_id=art_uuid,
)
db.add(media)
await db.commit()
await db.refresh(media)
return {
"id": str(media.id),
"url": public_url,
"filename": media.filename,
"media_type": media_type,
"size_bytes": media.size_bytes,
}
@router.get("")
async def list_media(
offset: int = Query(0, ge=0),
limit: int = Query(40, ge=1, le=200),
media_type: str | None = Query(None),
q: str | None = Query(None, max_length=200),
db: AsyncSession = Depends(get_db),
_: str = Depends(require_admin),
):
from sqlalchemy import func
from sqlalchemy.orm import outerjoin
base = select(Media)
if media_type:
base = base.where(Media.media_type == media_type)
if q:
base = base.where(Media.filename.ilike(f"%{q}%"))
total = (await db.execute(select(func.count()).select_from(base.subquery()))).scalar_one()
items = (await db.execute(base.order_by(Media.created_at.desc()).offset(offset).limit(limit))).scalars().all()
# Fetch article titles for media that have an article_id
art_ids = {m.article_id for m in items if m.article_id}
art_titles: dict = {}
if art_ids:
arts = (await db.execute(select(Article.id, Article.title).where(Article.id.in_(art_ids)))).all()
art_titles = {a.id: a.title for a in arts}
return {
"total": total,
"items": [
{
"id": str(m.id),
"url": m.url,
"filename": m.filename,
"media_type": m.media_type,
"size_bytes": m.size_bytes,
"created_at": m.created_at.isoformat(),
"article_id": str(m.article_id) if m.article_id else None,
"article_title": art_titles.get(m.article_id) if m.article_id else None,
}
for m in items
],
}
@router.delete("/{media_id}")
async def delete_media(
media_id: str,
db: AsyncSession = Depends(get_db),
_: str = Depends(require_admin),
):
import uuid as uuid_lib
media = (await db.execute(select(Media).where(Media.id == uuid_lib.UUID(media_id)))).scalar_one_or_none()
if not media:
raise HTTPException(status_code=404, detail="Not found")
object_name = media.url.split(f"/{MINIO_BUCKET}/", 1)[-1]
try:
client = get_minio()
client.remove_object(MINIO_BUCKET, object_name)
except Exception:
pass
await db.delete(media)
await db.commit()
return {"ok": True}