mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-05-07 08:22:42 +08:00
139 lines
5.1 KiB
Python
139 lines
5.1 KiB
Python
import contextlib
|
|
import re
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
import aiofiles
|
|
import httpx
|
|
from fastapi import HTTPException
|
|
|
|
from domain.plugins.types import PluginCreate, PluginManifestUpdate, PluginOut
|
|
from models.database import Plugin
|
|
|
|
|
|
class PluginService:
|
|
_plugins_root = Path("data/plugins")
|
|
|
|
@classmethod
|
|
def _folder_name(cls, rec: Plugin) -> str:
|
|
if rec.key:
|
|
safe = re.sub(r"[^A-Za-z0-9_.-]", "_", rec.key)
|
|
return safe or str(rec.id)
|
|
return str(rec.id)
|
|
|
|
@classmethod
|
|
def _bundle_dir_from_rec(cls, rec: Plugin) -> Path:
|
|
return cls._plugins_root / cls._folder_name(rec) / "current"
|
|
|
|
@classmethod
|
|
def _bundle_path_from_rec(cls, rec: Plugin) -> Path:
|
|
return cls._bundle_dir_from_rec(rec) / "index.js"
|
|
|
|
@classmethod
|
|
async def _download_bundle(cls, rec: Plugin, url: str) -> None:
|
|
dest_dir = cls._bundle_dir_from_rec(rec)
|
|
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
dest_path = cls._bundle_path_from_rec(rec)
|
|
tmp_path = dest_path.with_suffix(".tmp")
|
|
try:
|
|
async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
|
|
async with client.stream("GET", url) as resp:
|
|
resp.raise_for_status()
|
|
async with aiofiles.open(tmp_path, "wb") as f:
|
|
async for chunk in resp.aiter_bytes(chunk_size=65536):
|
|
if not chunk:
|
|
continue
|
|
await f.write(chunk)
|
|
tmp_path.replace(dest_path)
|
|
except Exception:
|
|
with contextlib.suppress(Exception):
|
|
if tmp_path.exists():
|
|
tmp_path.unlink()
|
|
raise
|
|
|
|
@classmethod
|
|
async def _ensure_bundle(cls, plugin_id: int) -> Path:
|
|
rec = await cls._get_or_404(plugin_id)
|
|
bundle_path = cls._bundle_path_from_rec(rec)
|
|
if bundle_path.exists():
|
|
return bundle_path
|
|
|
|
legacy = cls._plugins_root / str(rec.id) / "current" / "index.js"
|
|
if legacy.exists():
|
|
return legacy
|
|
|
|
raise HTTPException(status_code=404, detail="Plugin bundle not found")
|
|
|
|
@classmethod
|
|
async def get_bundle_path(cls, plugin_id: int) -> Path:
|
|
return await cls._ensure_bundle(plugin_id)
|
|
|
|
@classmethod
|
|
async def create(cls, payload: PluginCreate) -> PluginOut:
|
|
rec = await Plugin.create(**payload.model_dump())
|
|
try:
|
|
await cls._download_bundle(rec, rec.url)
|
|
except Exception as exc:
|
|
with contextlib.suppress(Exception):
|
|
await rec.delete()
|
|
raise HTTPException(status_code=400, detail=f"Failed to fetch plugin: {exc}")
|
|
return PluginOut.model_validate(rec)
|
|
|
|
@classmethod
|
|
async def list_plugins(cls) -> list[PluginOut]:
|
|
rows = await Plugin.all().order_by("-id")
|
|
return [PluginOut.model_validate(r) for r in rows]
|
|
|
|
@classmethod
|
|
async def _get_or_404(cls, plugin_id: int) -> Plugin:
|
|
rec = await Plugin.get_or_none(id=plugin_id)
|
|
if not rec:
|
|
raise HTTPException(status_code=404, detail="Plugin not found")
|
|
return rec
|
|
|
|
@classmethod
|
|
async def delete(cls, plugin_id: int) -> None:
|
|
rec = await cls._get_or_404(plugin_id)
|
|
await rec.delete()
|
|
with contextlib.suppress(Exception):
|
|
dirs = {cls._bundle_dir_from_rec(rec).parent, cls._plugins_root / str(rec.id)}
|
|
for plugin_dir in dirs:
|
|
if plugin_dir.exists():
|
|
shutil.rmtree(plugin_dir)
|
|
|
|
@classmethod
|
|
async def update(cls, plugin_id: int, payload: PluginCreate) -> PluginOut:
|
|
rec = await cls._get_or_404(plugin_id)
|
|
url_changed = rec.url != payload.url
|
|
if url_changed:
|
|
try:
|
|
await cls._download_bundle(rec, payload.url)
|
|
except Exception as exc:
|
|
raise HTTPException(status_code=400, detail=f"Failed to fetch plugin: {exc}")
|
|
rec.url = payload.url
|
|
rec.enabled = payload.enabled
|
|
await rec.save()
|
|
return PluginOut.model_validate(rec)
|
|
|
|
@classmethod
|
|
async def update_manifest(
|
|
cls, plugin_id: int, manifest: PluginManifestUpdate
|
|
) -> PluginOut:
|
|
rec = await cls._get_or_404(plugin_id)
|
|
old_dir = cls._bundle_dir_from_rec(rec).parent
|
|
updates = manifest.model_dump(exclude_none=True)
|
|
if updates:
|
|
for key, value in updates.items():
|
|
setattr(rec, key, value)
|
|
await rec.save()
|
|
new_dir = cls._bundle_dir_from_rec(rec).parent
|
|
if rec.key and new_dir != old_dir:
|
|
candidate_dir = old_dir if old_dir.exists() else (cls._plugins_root / str(rec.id))
|
|
if candidate_dir.exists():
|
|
new_dir.parent.mkdir(parents=True, exist_ok=True)
|
|
with contextlib.suppress(Exception):
|
|
if new_dir.exists():
|
|
shutil.rmtree(new_dir)
|
|
shutil.move(str(candidate_dir), str(new_dir))
|
|
return PluginOut.model_validate(rec)
|