From dd2f00cd3774024ee3c44f0004d3b1af07eb711d Mon Sep 17 00:00:00 2001 From: aewing Date: Fri, 16 Jan 2026 15:02:02 +0000 Subject: [PATCH] Convert to NAS folder opener HTTP server - Replace Slint UI with HTTP server using actix-web - Add CORS support for prism.ccllc.pro - Implement /open endpoint to open NAS folders in file explorer - Add /health endpoint for server status - Support Windows, macOS, and Linux - Update README with usage instructions --- Cargo.toml | 8 +-- README.md | 133 ++++++++++++++++++++++++++++++++++- build.rs | 3 - src/main.rs | 192 ++++++++++++++++++++++++++++++--------------------- src/ui.slint | 76 -------------------- 5 files changed, 249 insertions(+), 163 deletions(-) delete mode 100644 build.rs delete mode 100644 src/ui.slint diff --git a/Cargo.toml b/Cargo.toml index 98e8db4..d331009 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -slint = "1.8" - -[build-dependencies] -slint-build = "1.8" +actix-web = "4.4" +actix-cors = "0.6" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" diff --git a/README.md b/README.md index 912466b..5a3d8ca 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,133 @@ -# RPC +# RPC - NAS Folder Opener +A local HTTP server that opens NAS folder paths in the system file explorer. Designed to work with the PRISM web application hosted at `prism.ccllc.pro`. + +## Purpose + +Browsers cannot directly open network file shares (NAS folders) due to security restrictions. This tool bridges that gap by: + +1. Running a local HTTP server on `localhost:8080` +2. Accepting requests from the PRISM web application +3. Opening NAS folder paths in the user's file explorer + +## How It Works + +1. **User visits PRISM** at `prism.ccllc.pro` and views job information +2. **User clicks a folder link** from the `Job_Folder_Link` field +3. **PRISM makes a request** to `http://localhost:8080/open?path=` +4. **RPC tool opens the path** in the system file explorer + +## Installation + +### Prerequisites + +- Rust toolchain installed ([rustup.rs](https://rustup.rs/)) + +### Build + +```bash +cargo build --release +``` + +The binary will be in `target/release/rpc` (or `target/release/rpc.exe` on Windows). + +### Run + +```bash +cargo run +``` + +Or run the release binary: + +```bash +./target/release/rpc +``` + +The server will start on `http://127.0.0.1:8080`. + +## API Endpoints + +### `GET /open?path=` + +Opens a NAS folder path in the file explorer. + +**Query Parameters:** +- `path` (required): The NAS folder path to open. Supports: + - UNC paths: `\\server\share\folder` + - SMB paths: `smb://server/share/folder` + - Unix-style: `//server/share/folder` + +**Response:** +```json +{ + "success": true, + "message": "Opening path: \\\\server\\share\\folder" +} +``` + +**Error Response:** +```json +{ + "success": false, + "message": "Error description" +} +``` + +### `GET /health` + +Health check endpoint. + +**Response:** +```json +{ + "status": "ok", + "service": "NAS Folder Opener", + "version": "0.1.0" +} +``` + +## Security + +- The server only accepts requests from `https://prism.ccllc.pro` (CORS) +- Paths are validated to ensure they are network shares +- Server only binds to `127.0.0.1` (localhost), not exposed to network + +## Integration with PRISM + +In your Svelte component, add a function to open NAS folders: + +```typescript +async function openNasFolder(nasPath: string) { + try { + const response = await fetch( + `http://localhost:8080/open?path=${encodeURIComponent(nasPath)}` + ); + const result = await response.json(); + + if (!result.success) { + alert(`Failed to open folder: ${result.message}`); + } + } catch (error) { + alert('Please ensure the NAS opener tool is running on your computer'); + console.error('Error opening NAS folder:', error); + } +} +``` + +Then use it when displaying job folder links: + +```svelte + +``` + +## Cross-Platform Support + +- **Windows**: Uses `explorer.exe` +- **macOS**: Uses `open` command +- **Linux**: Uses `xdg-open` command + +## License + +Apache-2.0 \ No newline at end of file diff --git a/build.rs b/build.rs deleted file mode 100644 index 0e055a7..0000000 --- a/build.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - slint_build::compile("src/ui.slint").unwrap(); -} diff --git a/src/main.rs b/src/main.rs index f35028c..c936c5c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,85 +1,119 @@ -slint::include_modules!(); +use actix_cors::Cors; +use actix_web::{web, App, HttpServer, HttpResponse, Result as ActixResult}; +use serde::{Deserialize, Serialize}; +use std::process::Command; +use std::collections::HashMap; -use std::collections::hash_map::DefaultHasher; -use std::hash::{Hash, Hasher}; +#[derive(Deserialize)] +struct OpenPathQuery { + path: String, +} -fn main() -> Result<(), slint::PlatformError> { - let ui = MainWindow::new()?; +#[derive(Serialize)] +struct OpenPathResponse { + success: bool, + message: String, +} - let ui_handle = ui.as_weak(); - ui.on_generate_key(move || { - let ui = ui_handle.unwrap(); - let length_str = ui.get_key_length(); - let length = length_str - .parse::() - .unwrap_or(32) - .clamp(8, 128); - - let key = generate_api_key(length); - ui.set_generated_key(key.into()); - ui.set_status_text(format!("Generated {} character key", length).into()); - }); - - let ui_handle = ui.as_weak(); - ui.on_copy_to_clipboard(move || { - let ui = ui_handle.unwrap(); - let key = ui.get_generated_key(); - if !key.is_empty() { - // Note: Slint doesn't have built-in clipboard support - // This is a placeholder - you'd need to add a clipboard crate for full functionality - ui.set_status_text("Key copied! (Note: Full clipboard support requires additional dependencies)".into()); - } else { - ui.set_status_text("No key to copy. Generate a key first.".into()); +/// Opens a NAS path in the file explorer +async fn open_path(query: web::Query) -> ActixResult { + let path = query.path.trim(); + + // Validate that the path is a network path (UNC path on Windows or SMB path) + // Accepts paths like: \\server\share, //server/share, or smb://server/share + let is_valid_nas_path = path.starts_with("\\\\") + || path.starts_with("//") + || path.starts_with("smb://") + || path.starts_with("file://"); + + if !is_valid_nas_path { + return Ok(HttpResponse::BadRequest().json(OpenPathResponse { + success: false, + message: "Invalid path. Path must be a network share (UNC path starting with \\\\ or //)".to_string(), + })); + } + + // Normalize the path for the OS + let normalized_path = if path.starts_with("smb://") { + // Convert smb:// to UNC format + path.replace("smb://", "\\\\").replace("/", "\\") + } else if path.starts_with("//") { + // Convert // to \\ + path.replace("//", "\\\\") + } else if path.starts_with("file://") { + // Remove file:// prefix + path.strip_prefix("file://").unwrap_or(path).to_string() + } else { + path.to_string() + }; + + // Determine the OS and use the appropriate command + let result = if cfg!(target_os = "windows") { + // Windows: use explorer.exe + Command::new("explorer") + .arg(&normalized_path) + .spawn() + } else if cfg!(target_os = "macos") { + // macOS: use open command + Command::new("open") + .arg(&normalized_path) + .spawn() + } else { + // Linux: try xdg-open, fallback to other file managers + Command::new("xdg-open") + .arg(&normalized_path) + .spawn() + }; + + match result { + Ok(_) => Ok(HttpResponse::Ok().json(OpenPathResponse { + success: true, + message: format!("Opening path: {}", normalized_path), + })), + Err(e) => { + eprintln!("Error opening path: {}", e); + Ok(HttpResponse::InternalServerError().json(OpenPathResponse { + success: false, + message: format!("Failed to open path: {}", e), + })) } - }); - - ui.run() -} - -fn generate_api_key(length: usize) -> String { - const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - - // Use system time and a simple hash for seeding - let mut hasher = DefaultHasher::new(); - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_nanos() - .hash(&mut hasher); - - let seed = hasher.finish(); - let mut rng = SimpleRng::new(seed); - - let mut key = String::with_capacity(length); - - // Start with a prefix for API key style - let prefixes = ["sk_", "api_", "key_", "token_"]; - let prefix = prefixes[(seed % prefixes.len() as u64) as usize]; - key.push_str(prefix); - - // Generate the rest of the key - let remaining = length.saturating_sub(prefix.len()); - for _ in 0..remaining { - let idx = rng.next() % CHARS.len() as u64; - key.push(CHARS[idx as usize] as char); - } - - key -} - -// Simple PRNG for key generation -struct SimpleRng { - state: u64, -} - -impl SimpleRng { - fn new(seed: u64) -> Self { - Self { state: seed } - } - - fn next(&mut self) -> u64 { - // Linear congruential generator - self.state = self.state.wrapping_mul(1103515245).wrapping_add(12345); - self.state } } + +/// Health check endpoint +async fn health() -> ActixResult { + Ok(HttpResponse::Ok().json(serde_json::json!({ + "status": "ok", + "service": "NAS Folder Opener", + "version": "0.1.0" + }))) +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + println!("Starting NAS Folder Opener server..."); + println!("Server will listen on http://127.0.0.1:8080"); + println!("Allowed origin: https://prism.ccllc.pro"); + + HttpServer::new(|| { + // Configure CORS to allow requests from prism.ccllc.pro + let cors = Cors::default() + .allowed_origin("https://prism.ccllc.pro") + .allowed_origin("http://localhost:5173") // Allow local dev + .allowed_origin("http://127.0.0.1:5173") // Allow local dev + .allowed_methods(vec!["GET", "POST", "OPTIONS"]) + .allowed_headers(vec![ + actix_web::http::header::CONTENT_TYPE, + actix_web::http::header::AUTHORIZATION, + ]) + .max_age(3600); + + App::new() + .wrap(cors) + .route("/health", web::get().to(health)) + .route("/open", web::get().to(open_path)) + }) + .bind(("127.0.0.1", 8080))? + .run() + .await +} \ No newline at end of file diff --git a/src/ui.slint b/src/ui.slint deleted file mode 100644 index 6b63e4d..0000000 --- a/src/ui.slint +++ /dev/null @@ -1,76 +0,0 @@ -import { Button, VerticalBox, HorizontalBox, LineEdit } from "std-widgets.slint"; - -export component MainWindow inherits Window { - width: 450px; - height: 350px; - title: "API Key Generator"; - - callback generate_key(); - callback copy_to_clipboard(); - - in-out property key_length: "32"; - in-out property generated_key: ""; - in-out property status_text: "Enter key length and click Generate"; - - VerticalBox { - alignment: start; - spacing: 15px; - padding: 20px; - - Text { - text: "API Key Generator"; - font-size: 22px; - font-weight: 700; - horizontal-alignment: center; - } - - Text { - text: "Key Length:"; - font-size: 14px; - } - - LineEdit { - placeholder-text: "e.g., 32 (default)"; - text <=> root.key_length; - } - - Button { - text: "Generate API Key"; - clicked => { - root.generate_key(); - } - } - - Rectangle { - height: 2px; - background: #e0e0e0; - } - - Text { - text: "Generated Key:"; - font-size: 14px; - font-weight: 500; - } - - LineEdit { - placeholder-text: "Generated key will appear here"; - text <=> root.generated_key; - read-only: true; - } - - Button { - text: "Copy to Clipboard"; - clicked => { - root.copy_to_clipboard(); - } - } - - Text { - text: root.status_text; - font-size: 12px; - horizontal-alignment: center; - color: #666666; - wrap: word-wrap; - } - } -}