/
mcmare
/
RustAPI
Обзор
Документация
Войти
/
mcmare
/
RustAPI
Код
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
engine/src/webui/scripts.rs
126 строк
4 KB
mcmare
Add input/output schema for script endpoints, redesign WebUI with Tailwind
19 июл 2026, 09:28
19 июл 2026, 09:28
f3df0d8
Код
Авторство
О чём код?
use axum::{ extract::{Path as AxumPath, Query, State}, http::StatusCode, Json, }; use rustapi_core::ApiError; use serde::{Deserialize, Serialize}; use std::path::PathBuf; use super::WebuiState; use crate::config::EndpointKind; fn scripts_dir(config_dir: &std::path::Path) -> PathBuf { config_dir.join("scripts") } fn safe_filename(name: &str) -> Result<&str, ApiError> { if name.is_empty() || name.contains('/') || name.contains('\\') || name.contains("..") { return Err(ApiError::Validation(format!("invalid script filename '{name}'"))); } Ok(name) } /// Syntax-only check (host functions like `db`/`response` aren't registered on this bare /// engine) — enough to catch typos before they hit disk, not a substitute for actually /// running the script. pub(crate) fn check_syntax(source: &str) -> Result<(), ApiError> { rhai::Engine::new() .compile(source) .map(|_| ()) .map_err(|e| ApiError::Validation(format!("script does not parse: {e}"))) } pub(crate) fn write_script(config_dir: &std::path::Path, filename: &str, source: &str) -> Result<(), ApiError> { let filename = safe_filename(filename)?; check_syntax(source)?; let dir = scripts_dir(config_dir); std::fs::create_dir_all(&dir).map_err(|e| ApiError::Internal(e.into()))?; std::fs::write(dir.join(filename), source).map_err(|e| ApiError::Internal(e.into())) } #[derive(Serialize)] pub struct ScriptOut { filename: String, } pub async fn list(State(state): State<WebuiState>) -> Result<Json<Vec<ScriptOut>>, ApiError> { let dir = scripts_dir(&state.config_dir); if !dir.exists() { return Ok(Json(vec![])); } let mut out = Vec::new(); for entry in std::fs::read_dir(&dir).map_err(|e| ApiError::Internal(e.into()))? { let entry = entry.map_err(|e| ApiError::Internal(e.into()))?; let path = entry.path(); if path.extension().and_then(|e| e.to_str()) == Some("rhai") { if let Some(name) = path.file_name().and_then(|n| n.to_str()) { out.push(ScriptOut { filename: name.to_string() }); } } } out.sort_by(|a, b| a.filename.cmp(&b.filename)); Ok(Json(out)) } pub async fn read( State(state): State<WebuiState>, AxumPath(filename): AxumPath<String>, ) -> Result<String, ApiError> { let filename = safe_filename(&filename)?; let path = scripts_dir(&state.config_dir).join(filename); std::fs::read_to_string(&path).map_err(|_| ApiError::NotFound) } #[derive(Deserialize)] pub struct WriteScriptBody { source: String, } pub async fn write( State(state): State<WebuiState>, AxumPath(filename): AxumPath<String>, Json(body): Json<WriteScriptBody>, ) -> Result<StatusCode, ApiError> { write_script(&state.config_dir, &filename, &body.source)?; Ok(StatusCode::NO_CONTENT) } #[derive(Deserialize)] pub struct DeleteQuery { #[serde(default)] force: bool, } pub async fn remove( State(state): State<WebuiState>, AxumPath(filename): AxumPath<String>, Query(q): Query<DeleteQuery>, ) -> Result<StatusCode, ApiError> { let filename = safe_filename(&filename)?; let path = scripts_dir(&state.config_dir).join(filename); if !path.exists() { return Err(ApiError::NotFound); } if !q.force { let config = crate::config::load_config(&state.config_dir).map_err(ApiError::Internal)?; let referencing: Vec<String> = config .endpoints .iter() .filter_map(|ep| match &ep.kind { EndpointKind::Script { script_path, .. } if script_path == &path => Some(ep.path.clone()), _ => None, }) .collect(); if !referencing.is_empty() { return Err(ApiError::Conflict(format!( "still referenced by endpoints: {} (pass ?force=true to delete anyway)", referencing.join(", ") ))); } } std::fs::remove_file(&path).map_err(|e| ApiError::Internal(e.into()))?; Ok(StatusCode::NO_CONTENT) }