Files
news_all_spo/api/route/admin_system.py
jze9 9e277de65d fix: исправить URL медиа — nginx /media/ проксирует без имени бакета
- vk_parser: убрать bucket из MINIO_PUBLIC_URL (nginx добавляет /news-media/ сам)
- admin_system: добавить endpoint POST /repair-media-urls для обновления старых URL в БД
- SystemStatus: кнопка «Починить URL медиа» запускает repair endpoint
- .env.example: заменить реальные адреса/пароли на плейсхолдеры

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 16:44:36 +05:00

173 lines
6.1 KiB
Python

import os
import time
import asyncio
from urllib.parse import urlparse
from fastapi import APIRouter, Depends
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
from bd.database import get_db, get_redis, DATABASE_URL, REDIS_URL
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")
def _parse_db_url(url: str) -> dict:
"""Вытащить host/port/db/user из DATABASE_URL без пароля."""
try:
# postgresql+asyncpg://user:pass@host:port/db
p = urlparse(url.replace("+asyncpg", ""))
return {
"host": p.hostname or "?",
"port": p.port or 5432,
"database": (p.path or "/").lstrip("/") or "?",
"user": p.username or "?",
}
except Exception:
return {"host": "?", "port": 5432, "database": "?", "user": "?"}
def _parse_redis_url(url: str) -> dict:
"""Вытащить host/port из REDIS_URL без пароля."""
try:
p = urlparse(url)
return {
"host": p.hostname or "?",
"port": p.port or 6379,
"db": (p.path or "/0").lstrip("/") or "0",
}
except Exception:
return {"host": "?", "port": 6379, "db": "0"}
async def _check_postgres(db: AsyncSession) -> dict:
info = _parse_db_url(DATABASE_URL)
t0 = time.monotonic()
try:
result = await db.execute(text("SELECT version()"))
version = result.scalar() or ""
latency_ms = round((time.monotonic() - t0) * 1000, 1)
# Извлекаем короткую версию: "PostgreSQL 15.3 ..."
short_ver = version.split(",")[0] if version else "?"
return {"ok": True, "latency_ms": latency_ms, "version": short_ver, **info}
except Exception as e:
return {"ok": False, "error": str(e), "latency_ms": None, **info}
async def _check_redis() -> dict:
info = _parse_redis_url(REDIS_URL)
t0 = time.monotonic()
try:
redis = await get_redis()
pong = await redis.ping()
latency_ms = round((time.monotonic() - t0) * 1000, 1)
server_info = await redis.info("server")
version = server_info.get("redis_version", "?")
return {"ok": bool(pong), "latency_ms": latency_ms, "version": f"Redis {version}", **info}
except Exception as e:
return {"ok": False, "error": str(e), "latency_ms": None, **info}
async def _check_minio() -> dict:
t0 = time.monotonic()
try:
from minio import Minio
loop = asyncio.get_event_loop()
client = Minio(MINIO_ENDPOINT, access_key=MINIO_ACCESS_KEY, secret_key=MINIO_SECRET_KEY, secure=False)
exists = await loop.run_in_executor(None, client.bucket_exists, MINIO_BUCKET)
latency_ms = round((time.monotonic() - t0) * 1000, 1)
return {
"ok": True,
"latency_ms": latency_ms,
"endpoint": MINIO_ENDPOINT,
"bucket": MINIO_BUCKET,
"public_url": MINIO_PUBLIC_URL,
"bucket_exists": exists,
}
except Exception as e:
return {
"ok": False,
"error": str(e),
"latency_ms": None,
"endpoint": MINIO_ENDPOINT,
"bucket": MINIO_BUCKET,
"public_url": MINIO_PUBLIC_URL,
}
@router.get("/status")
async def system_status(db: AsyncSession = Depends(get_db), _: str = Depends(require_admin)):
pg, redis, minio = await asyncio.gather(
_check_postgres(db),
_check_redis(),
_check_minio(),
)
return {
"postgres": pg,
"redis": redis,
"minio": minio,
}
@router.post("/repair-media-urls")
async def repair_media_urls(db: AsyncSession = Depends(get_db), _: str = Depends(require_admin)):
"""Заменяет старые MinIO URL на актуальный MINIO_PUBLIC_URL во всех статьях и медиафайлах.
Системный nginx проксирует /media/ → MinIO bucket /news-media/, поэтому bucket исключается из URL.
Старый формат: http://host:port/news-media/object_path
Новый формат: MINIO_PUBLIC_URL/object_path (без bucket)
Запускать один раз после изменения MINIO_PUBLIC_URL в .env.
"""
new_base = MINIO_PUBLIC_URL.rstrip("/")
# cover_url: http://host/news-media/path → new_base/path
r_cover = await db.execute(text(r"""
UPDATE articles
SET cover_url = :new_base || '/' ||
regexp_replace(cover_url, '^https?://[^/]+(:[0-9]+)?/[^/]+/', '')
WHERE cover_url IS NOT NULL
AND cover_url LIKE '%/news-media/%'
AND cover_url NOT LIKE :prefix
RETURNING id
"""), {"new_base": new_base, "prefix": new_base + "%"})
# HTML-контент: src="http://host/news-media/path" → src="new_base/path"
r_content = await db.execute(text(r"""
UPDATE articles
SET content = regexp_replace(
content,
'https?://[^/]+(:[0-9]+)?/[^/]+/([^"'' ]+)',
:new_base || '/\2',
'g'
)
WHERE content LIKE '%/news-media/%'
AND content NOT LIKE :prefix
RETURNING id
"""), {"new_base": new_base, "prefix": "%" + new_base + "%"})
# media.url
r_media = await db.execute(text(r"""
UPDATE media
SET url = :new_base || '/' ||
regexp_replace(url, '^https?://[^/]+(:[0-9]+)?/[^/]+/', '')
WHERE url LIKE '%/news-media/%'
AND url NOT LIKE :prefix
RETURNING id
"""), {"new_base": new_base, "prefix": new_base + "%"})
await db.commit()
return {
"new_base": new_base,
"cover_url_fixed": r_cover.rowcount,
"content_fixed": r_content.rowcount,
"media_fixed": r_media.rowcount,
}