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 bd.database import get_db from bd.models import Media 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(...), 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() 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=file.filename or object_name, url=public_url, media_type=media_type, size_bytes=len(data), ) 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), db: AsyncSession = Depends(get_db), _: str = Depends(require_admin), ): from sqlalchemy import func base = select(Media) if media_type: base = base.where(Media.media_type == media_type) 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() 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()} 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}