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
This commit is contained in:
2026-01-16 15:02:02 +00:00
parent fad5d0991b
commit dd2f00cd37
5 changed files with 249 additions and 163 deletions
+4 -4
View File
@@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
slint = "1.8" actix-web = "4.4"
actix-cors = "0.6"
[build-dependencies] serde = { version = "1.0", features = ["derive"] }
slint-build = "1.8" serde_json = "1.0"
+132 -1
View File
@@ -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=<NAS_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=<NAS_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
<button onclick={() => openNasFolder(job.Job_Folder_Link)}>
Open Folder
</button>
```
## Cross-Platform Support
- **Windows**: Uses `explorer.exe`
- **macOS**: Uses `open` command
- **Linux**: Uses `xdg-open` command
## License
Apache-2.0
-3
View File
@@ -1,3 +0,0 @@
fn main() {
slint_build::compile("src/ui.slint").unwrap();
}
+113 -79
View File
@@ -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; #[derive(Deserialize)]
use std::hash::{Hash, Hasher}; struct OpenPathQuery {
path: String,
}
fn main() -> Result<(), slint::PlatformError> { #[derive(Serialize)]
let ui = MainWindow::new()?; struct OpenPathResponse {
success: bool,
message: String,
}
let ui_handle = ui.as_weak(); /// Opens a NAS path in the file explorer
ui.on_generate_key(move || { async fn open_path(query: web::Query<OpenPathQuery>) -> ActixResult<HttpResponse> {
let ui = ui_handle.unwrap(); let path = query.path.trim();
let length_str = ui.get_key_length();
let length = length_str // Validate that the path is a network path (UNC path on Windows or SMB path)
.parse::<usize>() // Accepts paths like: \\server\share, //server/share, or smb://server/share
.unwrap_or(32) let is_valid_nas_path = path.starts_with("\\\\")
.clamp(8, 128); || path.starts_with("//")
|| path.starts_with("smb://")
let key = generate_api_key(length); || path.starts_with("file://");
ui.set_generated_key(key.into());
ui.set_status_text(format!("Generated {} character key", length).into()); if !is_valid_nas_path {
}); return Ok(HttpResponse::BadRequest().json(OpenPathResponse {
success: false,
let ui_handle = ui.as_weak(); message: "Invalid path. Path must be a network share (UNC path starting with \\\\ or //)".to_string(),
ui.on_copy_to_clipboard(move || { }));
let ui = ui_handle.unwrap(); }
let key = ui.get_generated_key();
if !key.is_empty() { // Normalize the path for the OS
// Note: Slint doesn't have built-in clipboard support let normalized_path = if path.starts_with("smb://") {
// This is a placeholder - you'd need to add a clipboard crate for full functionality // Convert smb:// to UNC format
ui.set_status_text("Key copied! (Note: Full clipboard support requires additional dependencies)".into()); path.replace("smb://", "\\\\").replace("/", "\\")
} else { } else if path.starts_with("//") {
ui.set_status_text("No key to copy. Generate a key first.".into()); // 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<HttpResponse> {
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
}
-76
View File
@@ -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 <string> key_length: "32";
in-out property <string> generated_key: "";
in-out property <string> 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;
}
}
}