feat: add FastAPI app with config delivery endpoint
This commit is contained in:
+132
@@ -0,0 +1,132 @@
|
||||
import logging
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
from datetime import datetime
|
||||
from typing import AsyncGenerator
|
||||
|
||||
import yaml
|
||||
from fastapi import FastAPI, Depends, HTTPException
|
||||
from fastapi.responses import Response
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
|
||||
from models import Base, Config, Subscription, ExportLog, make_engine, make_session_factory
|
||||
from mihomo import MihomoClient
|
||||
from expander import expand_config, build_mihomo_config
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s %(levelname)s %(message)s",
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite+aiosqlite:////data/db/app.db")
|
||||
MIHOMO_API = os.environ.get("MIHOMO_API", "http://mihomo:9090")
|
||||
MIHOMO_SECRET = os.environ.get("MIHOMO_SECRET", "")
|
||||
MIHOMO_CONFIG_DIR = os.environ.get("MIHOMO_CONFIG_DIR", "/data/mihomo")
|
||||
|
||||
engine = make_engine(DATABASE_URL)
|
||||
SessionLocal = make_session_factory(engine)
|
||||
mihomo_client = MihomoClient(MIHOMO_API, MIHOMO_SECRET)
|
||||
templates = Jinja2Templates(
|
||||
directory=os.path.join(os.path.dirname(__file__), "templates")
|
||||
)
|
||||
|
||||
|
||||
async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
||||
async with SessionLocal() as session:
|
||||
yield session
|
||||
|
||||
|
||||
async def write_and_reload_mihomo(db: AsyncSession) -> None:
|
||||
result = await db.execute(select(Config))
|
||||
configs = result.scalars().all()
|
||||
config_yaml = build_mihomo_config([c.base_yaml for c in configs], MIHOMO_SECRET)
|
||||
config_path = os.path.join(MIHOMO_CONFIG_DIR, "config.yaml")
|
||||
tmp_path = config_path + ".tmp"
|
||||
os.makedirs(MIHOMO_CONFIG_DIR, exist_ok=True)
|
||||
with open(tmp_path, "w") as f:
|
||||
f.write(config_yaml)
|
||||
os.replace(tmp_path, config_path)
|
||||
logger.info("Wrote Mihomo config to %s", config_path)
|
||||
await mihomo_client.reload_config()
|
||||
logger.info("Mihomo config reloaded")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI): # type: ignore[type-arg]
|
||||
os.makedirs(MIHOMO_CONFIG_DIR, exist_ok=True)
|
||||
db_path = DATABASE_URL.split("///")[-1]
|
||||
if db_path:
|
||||
os.makedirs(os.path.dirname(db_path), exist_ok=True)
|
||||
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
await mihomo_client.wait_ready()
|
||||
|
||||
async with SessionLocal() as db:
|
||||
await write_and_reload_mihomo(db)
|
||||
|
||||
yield
|
||||
|
||||
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
|
||||
|
||||
@app.get("/config/{token}.yaml")
|
||||
async def get_config(
|
||||
token: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> Response:
|
||||
result = await db.execute(select(Config).where(Config.token == token))
|
||||
config = result.scalar_one_or_none()
|
||||
if not config:
|
||||
raise HTTPException(status_code=404, detail="Config not found")
|
||||
|
||||
result = await db.execute(
|
||||
select(Subscription).where(Subscription.config_id == config.id)
|
||||
)
|
||||
subscriptions = result.scalars().all()
|
||||
|
||||
provider_proxies: dict[str, list[dict]] = {}
|
||||
errors: list[str] = []
|
||||
|
||||
for sub in subscriptions:
|
||||
try:
|
||||
proxies = await mihomo_client.refresh_and_collect(sub.name, timeout=30)
|
||||
provider_proxies[sub.name] = proxies
|
||||
sub.last_fetched_at = datetime.utcnow()
|
||||
except Exception as exc:
|
||||
logger.error("Failed to refresh provider %s: %s", sub.name, exc)
|
||||
errors.append(f"{sub.name}: {exc}")
|
||||
|
||||
try:
|
||||
expanded = expand_config(config.base_yaml, provider_proxies)
|
||||
except Exception as exc:
|
||||
logger.error("Config expansion failed for token %s: %s", token, exc)
|
||||
db.add(
|
||||
ExportLog(
|
||||
config_id=config.id,
|
||||
node_count=0,
|
||||
success=False,
|
||||
error_message=str(exc),
|
||||
)
|
||||
)
|
||||
await db.commit()
|
||||
raise HTTPException(status_code=500, detail=f"Config expansion failed: {exc}")
|
||||
|
||||
node_count = sum(len(p) for p in provider_proxies.values())
|
||||
error_msg = "; ".join(errors) if errors else None
|
||||
db.add(
|
||||
ExportLog(
|
||||
config_id=config.id,
|
||||
node_count=node_count,
|
||||
success=not bool(errors),
|
||||
error_message=error_msg,
|
||||
)
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
return Response(content=expanded, media_type="application/x-yaml")
|
||||
Reference in New Issue
Block a user