5349f21752
New FastAPI microservice (port 8020) providing unified blob storage via PUT/GET/DELETE/LIST HTTP API. Local filesystem backend is the default (zero extra deps). S3-compatible and WebDAV backends are built in. Backend is switchable at runtime via POST /migrate, which copies all objects to the new backend, verifies each one, atomically switches, then cleans up the old backend. WebDAV XML parsing uses defusedxml to prevent XXE attacks. Wired into docker-compose (storage_data volume) and registered in the backend service-health poller as 'storage-service'. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
69 lines
2.5 KiB
Python
69 lines
2.5 KiB
Python
import asyncio
|
|
from contextlib import asynccontextmanager
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
from app.core.app_config import seed_builtin_themes
|
|
from app.core.config import settings
|
|
from app.database import AsyncSessionLocal
|
|
from app.routers import admin, auth, categories_proxy, documents_proxy, groups, plugins, profile, services, users
|
|
from app.routers import settings as settings_router
|
|
from app.services.group_bootstrap import ensure_service_admin_groups
|
|
from app.services.service_health import check_all, health_check_loop, register_services
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
await asyncio.to_thread(seed_builtin_themes)
|
|
register_services(
|
|
doc_service_url=settings.DOC_SERVICE_URL,
|
|
ai_service_url=settings.AI_SERVICE_URL,
|
|
storage_service_url=settings.STORAGE_SERVICE_URL,
|
|
)
|
|
# Create <service-id>-admin groups for every registered service (idempotent)
|
|
async with AsyncSessionLocal() as db:
|
|
await ensure_service_admin_groups(db)
|
|
# Run an initial check immediately so the first API response is accurate
|
|
await check_all()
|
|
task = asyncio.create_task(health_check_loop())
|
|
yield
|
|
task.cancel()
|
|
try:
|
|
await task
|
|
except asyncio.CancelledError:
|
|
pass
|
|
|
|
|
|
app = FastAPI(title=settings.PROJECT_NAME, version="0.1.0", lifespan=lifespan)
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=settings.CORS_ORIGINS,
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
|
|
app.include_router(users.router, prefix="/api/users", tags=["users"])
|
|
app.include_router(profile.router, prefix="/api/profile", tags=["profile"])
|
|
app.include_router(admin.router, prefix="/api/admin", tags=["admin"])
|
|
app.include_router(groups.router, prefix="/api/admin/groups", tags=["admin"])
|
|
app.include_router(settings_router.router, prefix="/api/settings", tags=["settings"])
|
|
app.include_router(services.router, prefix="/api/services", tags=["services"])
|
|
app.include_router(plugins.router, prefix="/api/plugins", tags=["plugins"])
|
|
# categories_proxy MUST be registered before documents_proxy —
|
|
# otherwise /api/documents/{path:path} swallows /api/documents/categories/*
|
|
app.include_router(
|
|
categories_proxy.router,
|
|
prefix="/api/documents/categories",
|
|
tags=["categories"],
|
|
)
|
|
app.include_router(documents_proxy.router, prefix="/api/documents", tags=["documents"])
|
|
|
|
|
|
@app.get("/api/health")
|
|
def health():
|
|
return {"status": "ok"}
|