fix teg
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
@@ -9,9 +10,11 @@ from sqlalchemy.orm import selectinload
|
||||
import redis.asyncio as aioredis
|
||||
|
||||
from bd.database import get_db, get_redis
|
||||
from bd.models import Article, Category, Tag, ArticleStatus
|
||||
from bd.models import Article, Category, Tag, ArticleStatus, Media
|
||||
from route.deps import require_admin
|
||||
|
||||
MINIO_BUCKET = os.getenv("MINIO_BUCKET", "news-media")
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@@ -208,6 +211,26 @@ async def delete_article(
|
||||
article = (await db.execute(select(Article).where(Article.id == uuid.UUID(article_id)))).scalar_one_or_none()
|
||||
if not article:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
|
||||
# Удаляем все связанные медиафайлы из MinIO и БД
|
||||
media_items = (await db.execute(
|
||||
select(Media).where(Media.article_id == article.id)
|
||||
)).scalars().all()
|
||||
|
||||
if media_items:
|
||||
from route.admin_media import get_minio
|
||||
try:
|
||||
client = get_minio()
|
||||
for m in media_items:
|
||||
obj = m.url.split(f"/{MINIO_BUCKET}/", 1)[-1]
|
||||
try:
|
||||
client.remove_object(MINIO_BUCKET, obj)
|
||||
except Exception:
|
||||
pass
|
||||
await db.delete(m)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
await db.delete(article)
|
||||
await db.commit()
|
||||
await _invalidate_cache(redis)
|
||||
|
||||
@@ -7,9 +7,10 @@ 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
|
||||
from bd.models import Media, Article
|
||||
from route.deps import require_admin
|
||||
|
||||
router = APIRouter()
|
||||
@@ -50,6 +51,7 @@ def ensure_bucket(client: Minio):
|
||||
@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),
|
||||
):
|
||||
@@ -63,7 +65,27 @@ async def upload_media(
|
||||
|
||||
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}"
|
||||
|
||||
# Определяем путь в 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)
|
||||
@@ -78,10 +100,11 @@ async def upload_media(
|
||||
public_url = f"{MINIO_PUBLIC_URL}/{MINIO_BUCKET}/{object_name}"
|
||||
|
||||
media = Media(
|
||||
filename=file.filename or object_name,
|
||||
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()
|
||||
@@ -101,20 +124,40 @@ 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()}
|
||||
{
|
||||
"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
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException # BackgroundTasks используется в history endpoints
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -88,9 +88,9 @@ async def delete_source(source_id: str, db: AsyncSession = Depends(get_db), _: s
|
||||
# ── Ручной запуск импорта ─────────────────────────────────────────────────────
|
||||
|
||||
@router.post("/import")
|
||||
async def vk_manual_import(background_tasks: BackgroundTasks, _: str = Depends(require_admin)):
|
||||
background_tasks.add_task(run_import)
|
||||
return {"ok": True, "message": "Импорт последних постов запущен в фоне"}
|
||||
async def vk_manual_import(_: str = Depends(require_admin)):
|
||||
result = await run_import()
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/import/history")
|
||||
|
||||
@@ -75,13 +75,15 @@ async def list_news(
|
||||
|
||||
if date_from:
|
||||
try:
|
||||
base = base.where(Article.published_at >= datetime.fromisoformat(date_from))
|
||||
dt = datetime.fromisoformat(date_from.replace("Z", "+00:00"))
|
||||
base = base.where(Article.published_at >= dt.replace(tzinfo=None))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if date_to:
|
||||
try:
|
||||
base = base.where(Article.published_at <= datetime.fromisoformat(date_to))
|
||||
dt = datetime.fromisoformat(date_to.replace("Z", "+00:00"))
|
||||
base = base.where(Article.published_at <= dt.replace(tzinfo=None))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
Reference in New Issue
Block a user