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"
|
||||
|
||||
[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"
|
||||
|
||||
@@ -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
|
||||
+109
-75
@@ -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::<usize>()
|
||||
.unwrap_or(32)
|
||||
.clamp(8, 128);
|
||||
/// Opens a NAS path in the file explorer
|
||||
async fn open_path(query: web::Query<OpenPathQuery>) -> ActixResult<HttpResponse> {
|
||||
let path = query.path.trim();
|
||||
|
||||
let key = generate_api_key(length);
|
||||
ui.set_generated_key(key.into());
|
||||
ui.set_status_text(format!("Generated {} character key", length).into());
|
||||
});
|
||||
// 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://");
|
||||
|
||||
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());
|
||||
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 {
|
||||
ui.set_status_text("No key to copy. Generate a key first.".into());
|
||||
}
|
||||
});
|
||||
path.to_string()
|
||||
};
|
||||
|
||||
ui.run()
|
||||
}
|
||||
// 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()
|
||||
};
|
||||
|
||||
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
|
||||
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),
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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