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:
+4
-4
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
+111
-77
@@ -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
|
|
||||||
.parse::<usize>()
|
|
||||||
.unwrap_or(32)
|
|
||||||
.clamp(8, 128);
|
|
||||||
|
|
||||||
let key = generate_api_key(length);
|
// Validate that the path is a network path (UNC path on Windows or SMB path)
|
||||||
ui.set_generated_key(key.into());
|
// Accepts paths like: \\server\share, //server/share, or smb://server/share
|
||||||
ui.set_status_text(format!("Generated {} character key", length).into());
|
let is_valid_nas_path = path.starts_with("\\\\")
|
||||||
});
|
|| path.starts_with("//")
|
||||||
|
|| path.starts_with("smb://")
|
||||||
|
|| path.starts_with("file://");
|
||||||
|
|
||||||
let ui_handle = ui.as_weak();
|
if !is_valid_nas_path {
|
||||||
ui.on_copy_to_clipboard(move || {
|
return Ok(HttpResponse::BadRequest().json(OpenPathResponse {
|
||||||
let ui = ui_handle.unwrap();
|
success: false,
|
||||||
let key = ui.get_generated_key();
|
message: "Invalid path. Path must be a network share (UNC path starting with \\\\ or //)".to_string(),
|
||||||
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());
|
// Normalize the path for the OS
|
||||||
} else {
|
let normalized_path = if path.starts_with("smb://") {
|
||||||
ui.set_status_text("No key to copy. Generate a key first.".into());
|
// 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<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
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user