//! 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, #[serde(default, skip_serializing_if = "Option::is_none")] pub port: Option, #[serde( default, rename = "identityFile", skip_serializing_if = "Option::is_none" )] pub identity_file: Option, #[serde( default, rename = "jumpHost", skip_serializing_if = "Option::is_none" )] pub jump_host: Option, #[serde( default, rename = "extraArgs", skip_serializing_if = "Option::is_none" )] pub extra_args: Option>, } fn hosts_path(app: &AppHandle) -> Result { 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> { 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 = 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(()) }