feat(P2.1): install index libraries by WARMING the shared cache (not the global dir)

New OSS warm_library hook; /api/libraries/install now calls it (with the
requester id for the anon policy) instead of mutating the shared global
libraries volume — so that volume stops growing and can be retired (P2.5).
Falls back to the legacy arduino-cli global install when no overlay is loaded
(OSS self-host parity).
This commit is contained in:
David Montero 2026-06-07 22:33:05 +02:00
parent 97f390719f
commit 02d396318f
2 changed files with 57 additions and 2 deletions

View File

@ -1,6 +1,7 @@
from fastapi import APIRouter, HTTPException, Query
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
from app.api.routes.compile import arduino_cli
from app.core.hooks import get_current_user_id, warm_library
router = APIRouter()
@ -36,11 +37,30 @@ async def search_libraries(q: str = Query(..., description="Search query for lib
raise HTTPException(status_code=500, detail=str(e))
@router.post("/install", response_model=InstallResponse)
async def install_library(request: InstallLibraryRequest):
async def install_library(
request: InstallLibraryRequest,
requester_id: str | None = Depends(get_current_user_id),
):
"""
Install a specific Arduino library by name.
On velxio.dev this WARMS the shared content-addressed cache (the global
mutable libraries volume is being retired) rather than mutating that volume;
the overlay also enforces the anonymous policy. With no overlay (OSS
self-host) it falls back to the legacy arduino-cli global install.
"""
try:
# P2.1 — prefer warming the content-addressed cache (no global write).
warmed = await warm_library(request.name, request.version, requester_id)
if warmed is not None:
return InstallResponse(
success=bool(warmed.get("success")),
error=warmed.get("error"),
stdout=warmed.get("stdout"),
fallback=warmed.get("fallback"),
requested_version=warmed.get("requested_version"),
)
# OSS / no overlay: legacy global install (self-host parity).
spec = f"{request.name}@{request.version}" if request.version else request.name
result = await arduino_cli.install_library(spec)
if not result["success"]:

View File

@ -202,6 +202,41 @@ async def get_project_owner(project_id: Optional[str]) -> Optional[str]:
return None
# ── warm_library ──────────────────────────────────────────────────────────────
# "Install" an index library by WARMING the shared content-addressed cache
# (install into a throwaway sketchbook -> publish to the cache) instead of
# mutating the single shared global libraries volume — so the global dir stops
# growing and can be retired. `requester_id` enforces the anon policy (an
# anonymous user may only use libraries already referenced by an example/project,
# i.e. already cached; warming a fresh uncached lib requires sign-in). Returns a
# result dict ({success, error?, ...}) or None when no overlay is loaded -> the
# OSS route falls back to its legacy arduino-cli global install (self-host parity).
WarmLibraryHook = Callable[..., Awaitable[Optional[dict]]]
_warm_library_hook: Optional[WarmLibraryHook] = None
def register_warm_library(hook: WarmLibraryHook) -> None:
"""Install the cache-warm library installer. Called by overlays in register_pro."""
global _warm_library_hook
_warm_library_hook = hook
async def warm_library(
name: str, version: Optional[str] = None, requester_id: Optional[str] = None
) -> Optional[dict]:
"""Warm an index library into the shared cache. None -> no overlay (the OSS
route does its legacy global install). Never raises."""
if _warm_library_hook is None:
return None
try:
return await _warm_library_hook(name=name, version=version, requester_id=requester_id)
except Exception:
logger.exception("warm_library hook failed")
return {"success": False, "error": "Library install failed."}
# ── lifespan startup ──────────────────────────────────────────────────────────
# Overlays that need to run async setup during FastAPI lifespan (DB init,
# table creation, legacy column migrations, etc.) register a coroutine here.