mirror of
https://github.com/infinition/Bjorn.git
synced 2026-03-19 02:00:24 +00:00
feat: Implement package management utilities with JSON endpoints for listing and uninstalling packages feat: Create plugin management utilities with endpoints for listing, configuring, and installing plugins feat: Develop schedule and trigger management utilities with CRUD operations for schedules and triggers
227 lines
9.0 KiB
Python
227 lines
9.0 KiB
Python
"""plugin_utils.py - Plugin management web API endpoints."""
|
|
|
|
import json
|
|
import logging
|
|
from urllib.parse import parse_qs, urlparse
|
|
|
|
from logger import Logger
|
|
|
|
logger = Logger(name="plugin_utils", level=logging.DEBUG)
|
|
|
|
|
|
class PluginUtils:
|
|
"""Web API handlers for plugin management."""
|
|
|
|
def __init__(self, shared_data):
|
|
self.shared_data = shared_data
|
|
|
|
@property
|
|
def _mgr(self):
|
|
return getattr(self.shared_data, 'plugin_manager', None)
|
|
|
|
def _write_json(self, handler, data, status=200):
|
|
payload = json.dumps(data, ensure_ascii=False).encode("utf-8")
|
|
handler.send_response(status)
|
|
handler.send_header("Content-Type", "application/json; charset=utf-8")
|
|
handler.send_header("Content-Length", str(len(payload)))
|
|
handler.end_headers()
|
|
try:
|
|
handler.wfile.write(payload)
|
|
except (BrokenPipeError, ConnectionResetError):
|
|
pass
|
|
|
|
# ── GET endpoints ────────────────────────────────────────────────
|
|
|
|
def list_plugins(self, handler):
|
|
"""GET /api/plugins/list - All plugins with status."""
|
|
try:
|
|
mgr = self._mgr
|
|
if not mgr:
|
|
self._write_json(handler, {"status": "ok", "data": []})
|
|
return
|
|
|
|
plugins = mgr.get_all_status()
|
|
self._write_json(handler, {"status": "ok", "data": plugins})
|
|
except Exception as e:
|
|
logger.error(f"list_plugins failed: {e}")
|
|
self._write_json(handler, {"status": "error", "message": "Internal server error"}, 500)
|
|
|
|
def get_plugin_config(self, handler):
|
|
"""GET /api/plugins/config?id=<plugin_id> - Config schema + current values."""
|
|
try:
|
|
query = urlparse(handler.path).query
|
|
params = parse_qs(query)
|
|
plugin_id = params.get("id", [None])[0]
|
|
|
|
if not plugin_id:
|
|
self._write_json(handler, {"status": "error", "message": "Missing 'id' parameter"}, 400)
|
|
return
|
|
|
|
mgr = self._mgr
|
|
if not mgr:
|
|
self._write_json(handler, {"status": "error", "message": "Plugin manager not available"}, 503)
|
|
return
|
|
|
|
# Get metadata for schema
|
|
meta = mgr._meta.get(plugin_id)
|
|
if not meta:
|
|
# Try to load from DB
|
|
db_rec = self.shared_data.db.get_plugin_config(plugin_id)
|
|
if db_rec:
|
|
meta = db_rec.get("meta", {})
|
|
else:
|
|
self._write_json(handler, {"status": "error", "message": "Plugin not found"}, 404)
|
|
return
|
|
|
|
schema = meta.get("config_schema", {})
|
|
current_values = mgr.get_config(plugin_id)
|
|
|
|
self._write_json(handler, {
|
|
"status": "ok",
|
|
"plugin_id": plugin_id,
|
|
"schema": schema,
|
|
"values": current_values,
|
|
})
|
|
except Exception as e:
|
|
logger.error(f"get_plugin_config failed: {e}")
|
|
self._write_json(handler, {"status": "error", "message": "Internal server error"}, 500)
|
|
|
|
def get_plugin_logs(self, handler):
|
|
"""GET /api/plugins/logs?id=<plugin_id> - Recent log lines (placeholder)."""
|
|
try:
|
|
query = urlparse(handler.path).query
|
|
params = parse_qs(query)
|
|
plugin_id = params.get("id", [None])[0]
|
|
|
|
if not plugin_id:
|
|
self._write_json(handler, {"status": "error", "message": "Missing 'id' parameter"}, 400)
|
|
return
|
|
|
|
# For now, return empty — full log filtering can be added later
|
|
# by filtering the main log file for [plugin.<plugin_id>] entries
|
|
self._write_json(handler, {
|
|
"status": "ok",
|
|
"plugin_id": plugin_id,
|
|
"logs": [],
|
|
"message": "Log filtering available via console SSE with [plugin.{id}] prefix"
|
|
})
|
|
except Exception as e:
|
|
logger.error(f"get_plugin_logs failed: {e}")
|
|
self._write_json(handler, {"status": "error", "message": "Internal server error"}, 500)
|
|
|
|
# ── POST endpoints (JSON body) ───────────────────────────────────
|
|
|
|
def toggle_plugin(self, data: dict) -> dict:
|
|
"""POST /api/plugins/toggle - {id, enabled}"""
|
|
try:
|
|
plugin_id = data.get("id")
|
|
enabled = data.get("enabled")
|
|
|
|
if not plugin_id:
|
|
return {"status": "error", "message": "Missing 'id' parameter"}
|
|
if enabled is None:
|
|
return {"status": "error", "message": "Missing 'enabled' parameter"}
|
|
|
|
mgr = self._mgr
|
|
if not mgr:
|
|
return {"status": "error", "message": "Plugin manager not available"}
|
|
|
|
mgr.toggle_plugin(plugin_id, bool(int(enabled)))
|
|
|
|
return {
|
|
"status": "ok",
|
|
"plugin_id": plugin_id,
|
|
"enabled": bool(int(enabled)),
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"toggle_plugin failed: {e}")
|
|
return {"status": "error", "message": "Internal server error"}
|
|
|
|
def save_config(self, data: dict) -> dict:
|
|
"""POST /api/plugins/config - {id, config: {...}}"""
|
|
try:
|
|
plugin_id = data.get("id")
|
|
config = data.get("config")
|
|
|
|
if not plugin_id:
|
|
return {"status": "error", "message": "Missing 'id' parameter"}
|
|
if config is None or not isinstance(config, dict):
|
|
return {"status": "error", "message": "Missing or invalid 'config' parameter"}
|
|
|
|
mgr = self._mgr
|
|
if not mgr:
|
|
return {"status": "error", "message": "Plugin manager not available"}
|
|
|
|
mgr.save_config(plugin_id, config)
|
|
|
|
return {"status": "ok", "plugin_id": plugin_id}
|
|
except ValueError as e:
|
|
return {"status": "error", "message": str(e)}
|
|
except Exception as e:
|
|
logger.error(f"save_config failed: {e}")
|
|
return {"status": "error", "message": "Internal server error"}
|
|
|
|
def uninstall_plugin(self, data: dict) -> dict:
|
|
"""POST /api/plugins/uninstall - {id}"""
|
|
try:
|
|
plugin_id = data.get("id")
|
|
if not plugin_id:
|
|
return {"status": "error", "message": "Missing 'id' parameter"}
|
|
|
|
mgr = self._mgr
|
|
if not mgr:
|
|
return {"status": "error", "message": "Plugin manager not available"}
|
|
|
|
return mgr.uninstall(plugin_id)
|
|
except Exception as e:
|
|
logger.error(f"uninstall_plugin failed: {e}")
|
|
return {"status": "error", "message": "Internal server error"}
|
|
|
|
# ── MULTIPART endpoints ──────────────────────────────────────────
|
|
|
|
def install_plugin(self, handler):
|
|
"""POST /api/plugins/install - multipart upload of .zip"""
|
|
try:
|
|
mgr = self._mgr
|
|
if not mgr:
|
|
self._write_json(handler, {"status": "error", "message": "Plugin manager not available"}, 503)
|
|
return
|
|
|
|
content_type = handler.headers.get('Content-Type', '')
|
|
content_length = int(handler.headers.get('Content-Length', 0))
|
|
|
|
if content_length <= 0 or content_length > 10 * 1024 * 1024: # 10MB max
|
|
self._write_json(handler, {"status": "error", "message": "Invalid file size (max 10MB)"}, 400)
|
|
return
|
|
|
|
body = handler.rfile.read(content_length)
|
|
|
|
# Extract zip bytes from multipart form data
|
|
zip_bytes = None
|
|
if 'multipart' in content_type:
|
|
boundary = content_type.split('boundary=')[1].encode() if 'boundary=' in content_type else None
|
|
if boundary:
|
|
parts = body.split(b'--' + boundary)
|
|
for part in parts:
|
|
if b'filename=' in part and b'.zip' in part.lower():
|
|
# Extract file data after double CRLF
|
|
if b'\r\n\r\n' in part:
|
|
zip_bytes = part.split(b'\r\n\r\n', 1)[1].rstrip(b'\r\n--')
|
|
break
|
|
|
|
if not zip_bytes:
|
|
# Maybe raw zip upload (no multipart)
|
|
if body[:4] == b'PK\x03\x04':
|
|
zip_bytes = body
|
|
else:
|
|
self._write_json(handler, {"status": "error", "message": "No .zip file found in upload"}, 400)
|
|
return
|
|
|
|
result = mgr.install_from_zip(zip_bytes)
|
|
status_code = 200 if result.get("status") == "ok" else 400
|
|
self._write_json(handler, result, status_code)
|
|
|
|
except Exception as e:
|
|
logger.error(f"install_plugin failed: {e}")
|
|
self._write_json(handler, {"status": "error", "message": "Internal server error"}, 500)
|