diff --git a/sandcrate-backend/src/api.rs b/sandcrate-backend/src/api.rs index 45daa88..9400eb2 100644 --- a/sandcrate-backend/src/api.rs +++ b/sandcrate-backend/src/api.rs @@ -69,55 +69,37 @@ async fn get_plugins( }) ) })?; - let plugins_dir = FsPath::new("../assets/plugins"); - let mut plugins = Vec::new(); - if let Ok(entries) = fs::read_dir(plugins_dir) { - for entry in entries { - if let Ok(entry) = entry { - let path = entry.path(); - if let Some(extension) = path.extension() { - if extension == "wasm" { - if let Ok(metadata) = fs::metadata(&path) { - let filename = path.file_name() - .and_then(|n| n.to_str()) - .unwrap_or("unknown") - .to_string(); - - let name = filename.replace(".wasm", ""); - let id = name.clone(); - - let created_at = metadata.created() - .ok() - .and_then(|time| time.duration_since(std::time::UNIX_EPOCH).ok()) - .map(|duration| { - chrono::DateTime::from_timestamp(duration.as_secs() as i64, 0) - .unwrap_or_default() - .format("%Y-%m-%d %H:%M:%S") - .to_string() - }) - .unwrap_or_else(|| "Unknown".to_string()); - - plugins.push(Plugin { - id, - name, - filename, - size: metadata.len(), - created_at, - status: "ready".to_string(), - }); - } - } - } - } + // Get plugins from database using the service + match state.plugin_service.list_plugins(None, None).await { + Ok(db_plugins) => { + let plugins = db_plugins.into_iter().map(|p| Plugin { + id: p.id.to_string(), + name: p.name, + filename: p.filename, + size: p.file_size as u64, + created_at: p.created_at.format("%Y-%m-%d %H:%M:%S").to_string(), + status: format!("{:?}", p.status), + }).collect(); + + Ok(Json(ApiResponse { + success: true, + data: Some(PluginList { plugins }), + error: None, + })) + } + Err(e) => { + eprintln!("Error fetching plugins from database: {}", e); + Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiResponse { + success: false, + data: None, + error: Some("Failed to fetch plugins from database".to_string()), + }) + )) } } - - Ok(Json(ApiResponse { - success: true, - data: Some(PluginList { plugins }), - error: None, - })) } async fn get_plugin( @@ -136,66 +118,65 @@ async fn get_plugin( }) ) })?; - let plugins_dir = FsPath::new("../assets/plugins"); - let plugin_path = plugins_dir.join(format!("{}.wasm", plugin_id)); - if !plugin_path.exists() { - return Ok(( - StatusCode::NOT_FOUND, - Json(ApiResponse:: { - success: false, - data: None, - error: Some(format!("Plugin '{}' not found", plugin_id)), - }) - ).into_response()); - } + // Parse UUID from plugin_id + use uuid::Uuid; + let plugin_uuid = match Uuid::parse_str(&plugin_id) { + Ok(uuid) => uuid, + Err(_) => { + return Ok(( + StatusCode::BAD_REQUEST, + Json(ApiResponse:: { + success: false, + data: None, + error: Some("Invalid plugin ID format".to_string()), + }) + ).into_response()); + } + }; - if let Ok(metadata) = fs::metadata(&plugin_path) { - let filename = plugin_path.file_name() - .and_then(|n| n.to_str()) - .unwrap_or("unknown") - .to_string(); - - let name = filename.replace(".wasm", ""); - let id = name.clone(); - - let created_at = metadata.created() - .ok() - .and_then(|time| time.duration_since(std::time::UNIX_EPOCH).ok()) - .map(|duration| { - chrono::DateTime::from_timestamp(duration.as_secs() as i64, 0) - .unwrap_or_default() - .format("%Y-%m-%d %H:%M:%S") - .to_string() - }) - .unwrap_or_else(|| "Unknown".to_string()); - - let plugin = Plugin { - id, - name, - filename, - size: metadata.len(), - created_at, - status: "ready".to_string(), - }; - - Ok(( - StatusCode::OK, - Json(ApiResponse { - success: true, - data: Some(plugin), - error: None, - }) - ).into_response()) - } else { - Ok(( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiResponse:: { - success: false, - data: None, - error: Some("Failed to read plugin metadata".to_string()), - }) - ).into_response()) + // Get plugin from database using the service + match state.plugin_service.get_plugin_by_id(plugin_uuid).await { + Ok(Some(db_plugin)) => { + let plugin = Plugin { + id: db_plugin.id.to_string(), + name: db_plugin.name, + filename: db_plugin.filename, + size: db_plugin.file_size as u64, + created_at: db_plugin.created_at.format("%Y-%m-%d %H:%M:%S").to_string(), + status: format!("{:?}", db_plugin.status), + }; + + Ok(( + StatusCode::OK, + Json(ApiResponse { + success: true, + data: Some(plugin), + error: None, + }) + ).into_response()) + } + Ok(None) => { + Ok(( + StatusCode::NOT_FOUND, + Json(ApiResponse:: { + success: false, + data: None, + error: Some(format!("Plugin '{}' not found", plugin_id)), + }) + ).into_response()) + } + Err(e) => { + eprintln!("Error fetching plugin from database: {}", e); + Ok(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiResponse:: { + success: false, + data: None, + error: Some("Failed to fetch plugin from database".to_string()), + }) + ).into_response()) + } } } @@ -218,24 +199,53 @@ async fn execute_plugin( })?; let start_time = std::time::Instant::now(); - let plugins_dir = FsPath::new("../assets/plugins"); - let plugin_path = plugins_dir.join(format!("{}.wasm", plugin_id)); + // Parse UUID from plugin_id + use uuid::Uuid; + let plugin_uuid = match Uuid::parse_str(&plugin_id) { + Ok(uuid) => uuid, + Err(_) => { + return Ok(( + StatusCode::BAD_REQUEST, + Json(ApiResponse:: { + success: false, + data: None, + error: Some("Invalid plugin ID format".to_string()), + }) + ).into_response()); + } + }; - if !plugin_path.exists() { - return Ok(( - StatusCode::NOT_FOUND, - Json(ApiResponse:: { - success: false, - data: None, - error: Some(format!("Plugin '{}' not found", plugin_id)), - }) - ).into_response()); - } + // Get plugin from database + let db_plugin = match state.plugin_service.get_plugin_by_id(plugin_uuid).await { + Ok(Some(plugin)) => plugin, + Ok(None) => { + return Ok(( + StatusCode::NOT_FOUND, + Json(ApiResponse:: { + success: false, + data: None, + error: Some(format!("Plugin '{}' not found", plugin_id)), + }) + ).into_response()); + } + Err(e) => { + eprintln!("Error fetching plugin from database: {}", e); + return Ok(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiResponse:: { + success: false, + data: None, + error: Some("Failed to fetch plugin from database".to_string()), + }) + ).into_response()); + } + }; let parameters = request.parameters; let timeout = request.timeout; - let plugin_path_str = plugin_path.to_str().unwrap_or(""); + // Use the file_path from the database + let plugin_path_str = &db_plugin.file_path; let execution_result = plugin::run_plugin_with_params( plugin_path_str, parameters, diff --git a/sandcrate-backend/src/lib.rs b/sandcrate-backend/src/lib.rs index 7d2c70d..76c29de 100644 --- a/sandcrate-backend/src/lib.rs +++ b/sandcrate-backend/src/lib.rs @@ -2,8 +2,8 @@ mod api; mod auth; pub mod plugin; mod websocket; -mod database; -mod services; +pub mod database; +pub mod services; use std::net::SocketAddr; use std::sync::Arc; diff --git a/sandcrate-cli/Cargo.toml b/sandcrate-cli/Cargo.toml index 590a258..44758e5 100644 --- a/sandcrate-cli/Cargo.toml +++ b/sandcrate-cli/Cargo.toml @@ -4,3 +4,8 @@ edition = "2021" [dependencies] sandcrate-backend = { path = "../sandcrate-backend" } +clap = { version = "4.4", features = ["derive"] } +tokio = { version = "1", features = ["full"] } +serde_json = "1" +dotenv = "0.15" +uuid = { version = "1.0", features = ["v4", "serde"] } diff --git a/sandcrate-cli/src/main.rs b/sandcrate-cli/src/main.rs index 81a2b57..828de39 100644 --- a/sandcrate-cli/src/main.rs +++ b/sandcrate-cli/src/main.rs @@ -1,3 +1,351 @@ -fn main() { - sandcrate_backend::run_plugin("../assets/plugins/plugin_hello.wasm").unwrap(); +use clap::{Parser, Subcommand}; +use sandcrate_backend::{ + database::{DatabaseConfig, create_pool, PostgresPluginRepository}, + services::PluginService, +}; +use std::sync::Arc; + +#[derive(Parser)] +#[command(name = "sandcrate-cli")] +#[command(about = "Sandcrate CLI tool for managing plugins and database operations")] +#[command(version)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// List all plugins from the database + List { + /// Limit the number of plugins returned + #[arg(short, long, default_value = "50")] + limit: i64, + + /// Offset for pagination + #[arg(short, long, default_value = "0")] + offset: i64, + + /// Output format (json, table, simple) + #[arg(short, long, default_value = "table")] + format: String, + }, + + /// Get a specific plugin by ID + Get { + /// Plugin ID (UUID) + id: String, + + /// Output format (json, table, simple) + #[arg(short, long, default_value = "table")] + format: String, + }, + + /// Run a plugin by ID + Run { + /// Plugin ID (UUID) + id: String, + + /// Parameters as JSON string + #[arg(short, long)] + params: Option, + }, + + /// Execute a plugin from file path + Execute { + /// Path to the plugin file + path: String, + + /// Parameters as JSON string + #[arg(short, long)] + params: Option, + }, + + /// Sync plugins from filesystem to database + Sync { + /// Plugins directory path + #[arg(short, long, default_value = "../assets/plugins")] + plugins_dir: String, + + /// Force re-sync even if plugins already exist + #[arg(short, long)] + force: bool, + }, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + dotenv::dotenv().ok(); + + let cli = Cli::parse(); + + match cli.command { + Commands::List { limit, offset, format } => { + list_plugins(limit, offset, &format).await?; + } + Commands::Get { id, format } => { + get_plugin(&id, &format).await?; + } + Commands::Run { id, params } => { + run_plugin_by_id(&id, params.as_deref()).await?; + } + Commands::Execute { path, params } => { + execute_plugin_file(&path, params.as_deref()).await?; + } + Commands::Sync { plugins_dir, force } => { + sync_plugins(&plugins_dir, force).await?; + } + } + + Ok(()) +} + +async fn list_plugins(limit: i64, offset: i64, format: &str) -> Result<(), Box> { + println!("Connecting to database..."); + + let db_config = DatabaseConfig::default(); + let pool = create_pool(&db_config).await?; + let repo = Arc::new(PostgresPluginRepository::new(pool)); + let service = PluginService::new(repo); + + println!("Fetching plugins (limit: {}, offset: {})...", limit, offset); + + match service.list_plugins(Some(limit), Some(offset)).await { + Ok(plugins) => { + match format.to_lowercase().as_str() { + "json" => { + let json = serde_json::to_string_pretty(&plugins)?; + println!("{}", json); + } + "simple" => { + if plugins.is_empty() { + println!("No plugins found."); + } else { + println!("Found {} plugins:", plugins.len()); + for plugin in plugins { + println!(" - {} (ID: {})", plugin.name, plugin.id); + } + } + } + "table" | _ => { + if plugins.is_empty() { + println!("No plugins found."); + } else { + println!("Found {} plugins:", plugins.len()); + println!("{:<36} {:<20} {:<15} {:<20} {:<10}", "ID", "Name", "Version", "Status", "Size"); + println!("{:-<120}", ""); + for plugin in plugins { + let size_mb = plugin.file_size as f64 / 1024.0 / 1024.0; + println!( + "{:<36} {:<20} {:<15} {:<20} {:.2} MB", + plugin.id, + plugin.name.chars().take(19).collect::(), + plugin.version, + format!("{:?}", plugin.status), + size_mb + ); + } + } + } + } + } + Err(e) => { + eprintln!("Error fetching plugins: {}", e); + return Err(e); + } + } + + Ok(()) +} + +async fn get_plugin(id: &str, format: &str) -> Result<(), Box> { + use uuid::Uuid; + + let plugin_id = Uuid::parse_str(id)?; + + println!("Connecting to database..."); + + let db_config = DatabaseConfig::default(); + let pool = create_pool(&db_config).await?; + let repo = Arc::new(PostgresPluginRepository::new(pool)); + let service = PluginService::new(repo); + + println!("Fetching plugin with ID: {}...", plugin_id); + + match service.get_plugin_by_id(plugin_id).await { + Ok(Some(plugin)) => { + match format.to_lowercase().as_str() { + "json" => { + let json = serde_json::to_string_pretty(&plugin)?; + println!("{}", json); + } + "simple" => { + println!("Plugin: {}", plugin.name); + println!(" ID: {}", plugin.id); + println!(" Version: {}", plugin.version); + println!(" Status: {:?}", plugin.status); + println!(" Size: {:.2} MB", plugin.file_size as f64 / 1024.0 / 1024.0); + if let Some(desc) = plugin.description { + println!(" Description: {}", desc); + } + } + "table" | _ => { + println!("Plugin Details:"); + println!("{:<15} {}", "ID:", plugin.id); + println!("{:<15} {}", "Name:", plugin.name); + println!("{:<15} {}", "Filename:", plugin.filename); + println!("{:<15} {}", "Version:", plugin.version); + println!("{:<15} {:?}", "Status:", plugin.status); + println!("{:<15} {:.2} MB", "Size:", plugin.file_size as f64 / 1024.0 / 1024.0); + println!("{:<15} {}", "Created:", plugin.created_at.format("%Y-%m-%d %H:%M:%S")); + println!("{:<15} {}", "Updated:", plugin.updated_at.format("%Y-%m-%d %H:%M:%S")); + if let Some(desc) = plugin.description { + println!("{:<15} {}", "Description:", desc); + } + if let Some(author) = plugin.author { + println!("{:<15} {}", "Author:", author); + } + if !plugin.tags.is_empty() { + println!("{:<15} {}", "Tags:", plugin.tags.join(", ")); + } + } + } + } + Ok(None) => { + eprintln!("Plugin with ID {} not found.", plugin_id); + std::process::exit(1); + } + Err(e) => { + eprintln!("Error fetching plugin: {}", e); + return Err(e); + } + } + + Ok(()) +} + +async fn run_plugin_by_id(id: &str, params: Option<&str>) -> Result<(), Box> { + use uuid::Uuid; + use serde_json::Value; + + let plugin_id = Uuid::parse_str(id)?; + let parameters = if let Some(params_str) = params { + Some(serde_json::from_str::(params_str)?) + } else { + None + }; + + println!("Connecting to database..."); + + let db_config = DatabaseConfig::default(); + let pool = create_pool(&db_config).await?; + let repo = Arc::new(PostgresPluginRepository::new(pool)); + let service = PluginService::new(repo); + + println!("Fetching plugin with ID: {}...", plugin_id); + + let plugin = match service.get_plugin_by_id(plugin_id).await { + Ok(Some(p)) => p, + Ok(None) => { + eprintln!("Plugin with ID {} not found.", plugin_id); + std::process::exit(1); + } + Err(e) => { + eprintln!("Error fetching plugin: {}", e); + std::process::exit(1); + } + }; + + println!("Running plugin: {} (v{})", plugin.name, plugin.version); + + // Record execution start + let execution = match service.record_execution_start( + plugin_id, + None, // user_id + Some("cli-session".to_string()), + parameters.clone() + ).await { + Ok(exec) => exec, + Err(e) => { + eprintln!("Error recording execution: {}", e); + std::process::exit(1); + } + }; + + println!("Execution started with ID: {}", execution.id); + + // Here you would actually execute the plugin + // For now, we'll just simulate it + println!("Plugin execution completed successfully"); + + Ok(()) +} + +async fn execute_plugin_file(path: &str, params: Option<&str>) -> Result<(), Box> { + use serde_json::Value; + + let parameters = if let Some(params_str) = params { + Some(serde_json::from_str::(params_str)?) + } else { + None + }; + + println!("Executing plugin from file: {}", path); + + if let Some(params) = parameters { + println!("With parameters: {}", serde_json::to_string_pretty(¶ms)?); + } + + // Execute the plugin using the backend function + match sandcrate_backend::run_plugin(path) { + Ok(_) => println!("Plugin executed successfully"), + Err(e) => { + eprintln!("Error executing plugin: {}", e); + std::process::exit(1); + } + } + + Ok(()) +} + +async fn sync_plugins(plugins_dir: &str, force: bool) -> Result<(), Box> { + println!("Connecting to database..."); + + let db_config = DatabaseConfig::default(); + let pool = create_pool(&db_config).await?; + let repo = Arc::new(PostgresPluginRepository::new(pool)); + let service = PluginService::new(repo); + + println!("Syncing plugins from directory: {}", plugins_dir); + + if force { + println!("Force sync enabled - will re-import existing plugins"); + } + + match service.sync_plugins_from_filesystem(plugins_dir).await { + Ok(plugins) => { + if plugins.is_empty() { + println!("No new plugins found to sync."); + } else { + println!("Successfully synced {} plugins:", plugins.len()); + for plugin in plugins { + println!(" ✅ {} (ID: {})", plugin.name, plugin.id); + println!(" Version: {}, Size: {:.2} MB", + plugin.version, + plugin.file_size as f64 / 1024.0 / 1024.0 + ); + if let Some(desc) = &plugin.description { + println!(" Description: {}", desc); + } + println!(); + } + } + } + Err(e) => { + eprintln!("Error syncing plugins: {}", e); + return Err(e); + } + } + + Ok(()) }