Files
Bjorn/web_utils/plugin_utils.py
infinition b0584a1a8e feat: Add login page with dynamic RGB effects and password toggle functionality
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
2026-03-19 00:40:04 +01:00

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)