115 lines
3.7 KiB
Rust
115 lines
3.7 KiB
Rust
use serde::{Deserialize, Serialize};
|
|
use std::fs;
|
|
use std::path::{Path, PathBuf};
|
|
use uuid::Uuid;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
pub struct AppConfig {
|
|
pub host_id: String,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub display_name: Option<String>,
|
|
}
|
|
|
|
impl Default for AppConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
host_id: Uuid::new_v4().to_string(),
|
|
display_name: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Resolves configuration path according to platform conventions:
|
|
/// - Windows: `%APPDATA%\HermesPair\config.json`
|
|
/// - Linux/Unix: `~/.config/hermes-pair/config.json`
|
|
pub fn get_config_path() -> PathBuf {
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
if let Ok(appdata) = std::env::var("APPDATA") {
|
|
return PathBuf::from(appdata)
|
|
.join("HermesPair")
|
|
.join("config.json");
|
|
}
|
|
if let Some(config_dir) = dirs::config_dir() {
|
|
return config_dir.join("HermesPair").join("config.json");
|
|
}
|
|
PathBuf::from("config.json")
|
|
}
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
{
|
|
if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
|
|
return PathBuf::from(xdg).join("hermes-pair").join("config.json");
|
|
}
|
|
if let Some(config_dir) = dirs::config_dir() {
|
|
return config_dir.join("hermes-pair").join("config.json");
|
|
}
|
|
if let Some(home_dir) = dirs::home_dir() {
|
|
return home_dir
|
|
.join(".config")
|
|
.join("hermes-pair")
|
|
.join("config.json");
|
|
}
|
|
PathBuf::from("config.json")
|
|
}
|
|
}
|
|
|
|
/// Saves the configuration atomically by writing to a temporary file and renaming it.
|
|
pub fn save_config_to_path(config: &AppConfig, path: &Path) -> Result<(), std::io::Error> {
|
|
if let Some(parent) = path.parent() {
|
|
fs::create_dir_all(parent)?;
|
|
}
|
|
|
|
let json_bytes = serde_json::to_vec_pretty(config)
|
|
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
|
|
|
|
let tmp_file_name = format!(
|
|
"{}.tmp.{}",
|
|
path.file_name()
|
|
.and_then(|n| n.to_str())
|
|
.unwrap_or("config"),
|
|
Uuid::new_v4()
|
|
);
|
|
let tmp_path = match path.parent() {
|
|
Some(p) => p.join(tmp_file_name),
|
|
None => PathBuf::from(tmp_file_name),
|
|
};
|
|
|
|
fs::write(&tmp_path, json_bytes)?;
|
|
|
|
// On Windows and Unix, fs::rename replaces the destination atomically if in the same directory.
|
|
if let Err(_err) = fs::rename(&tmp_path, path) {
|
|
// Fallback in case rename fails due to cross-platform replacement edge cases
|
|
let _ = fs::remove_file(path);
|
|
if let Err(fallback_err) = fs::rename(&tmp_path, path) {
|
|
let _ = fs::remove_file(&tmp_path);
|
|
return Err(fallback_err);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Loads an existing config or generates a new one with a persistent UUIDv4 `host_id` and saves it.
|
|
pub fn load_or_create_config_from_path(path: &Path) -> Result<AppConfig, std::io::Error> {
|
|
if path.exists() {
|
|
let content = fs::read_to_string(path)?;
|
|
if let Ok(mut config) = serde_json::from_str::<AppConfig>(&content) {
|
|
if config.host_id.trim().is_empty() {
|
|
config.host_id = Uuid::new_v4().to_string();
|
|
save_config_to_path(&config, path)?;
|
|
}
|
|
return Ok(config);
|
|
}
|
|
}
|
|
|
|
let new_config = AppConfig::default();
|
|
save_config_to_path(&new_config, path)?;
|
|
Ok(new_config)
|
|
}
|
|
|
|
/// Convenience function to load or create configuration at the default system location.
|
|
pub fn load_or_create_config() -> Result<AppConfig, std::io::Error> {
|
|
let path = get_config_path();
|
|
load_or_create_config_from_path(&path)
|
|
}
|