Add SSH connections: saved hosts manager and hierarchical shell picker
This commit is contained in:
parent
4e5bc7e081
commit
872fb0e80e
14 changed files with 1324 additions and 171 deletions
74
src-tauri/src/hosts.rs
Normal file
74
src-tauri/src/hosts.rs
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
//! Saved SSH hosts. Persisted to `%APPDATA%\com.megaproxy.tiletopia\hosts.json`
|
||||
//! alongside `workspace.json`. The frontend owns the in-memory state and the
|
||||
//! add/edit/delete UX; the backend just reads/writes the whole list.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
const HOSTS_FILE: &str = "hosts.json";
|
||||
|
||||
/// One saved host. Fields beyond `hostname` are optional; ssh.exe will fall
|
||||
/// back to `~/.ssh/config` and its own defaults for anything we don't pass.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SshHost {
|
||||
pub id: String,
|
||||
pub label: String,
|
||||
pub hostname: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub user: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub port: Option<u16>,
|
||||
#[serde(
|
||||
default,
|
||||
rename = "identityFile",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub identity_file: Option<String>,
|
||||
#[serde(
|
||||
default,
|
||||
rename = "jumpHost",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub jump_host: Option<String>,
|
||||
#[serde(
|
||||
default,
|
||||
rename = "extraArgs",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub extra_args: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
fn hosts_path(app: &AppHandle) -> Result<PathBuf> {
|
||||
let dir = app
|
||||
.path()
|
||||
.app_config_dir()
|
||||
.map_err(|e| anyhow::anyhow!("app_config_dir: {e}"))?;
|
||||
Ok(dir.join(HOSTS_FILE))
|
||||
}
|
||||
|
||||
pub fn load(app: &AppHandle) -> Result<Vec<SshHost>> {
|
||||
let path = hosts_path(app)?;
|
||||
if !path.exists() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let raw = std::fs::read_to_string(&path).context("read hosts.json")?;
|
||||
let hosts: Vec<SshHost> = serde_json::from_str(&raw).context("parse hosts.json")?;
|
||||
Ok(hosts)
|
||||
}
|
||||
|
||||
pub fn save(app: &AppHandle, hosts: &[SshHost]) -> Result<()> {
|
||||
let path = hosts_path(app)?;
|
||||
if let Some(dir) = path.parent() {
|
||||
std::fs::create_dir_all(dir).context("create_dir_all")?;
|
||||
}
|
||||
let tmp = path.with_extension("json.tmp");
|
||||
let json = serde_json::to_string_pretty(hosts).context("serialize hosts")?;
|
||||
std::fs::write(&tmp, json.as_bytes()).context("write tmp hosts.json")?;
|
||||
// `std::fs::rename` is atomic on Unix and uses MoveFileEx with
|
||||
// REPLACE_EXISTING on Windows — same pattern as save_workspace.
|
||||
std::fs::rename(&tmp, &path).context("rename hosts.json")?;
|
||||
Ok(())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue