use serde::{Deserialize, Serialize};
use std::env;
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct GlobalXbpPaths {
pub root_dir: PathBuf,
pub config_file: PathBuf,
pub ssh_dir: PathBuf,
pub cache_dir: PathBuf,
pub logs_dir: PathBuf,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SshConfig {
pub password: Option<String>,
pub username: Option<String>,
pub host: Option<String>,
pub project_dir: Option<String>,
}
impl SshConfig {
pub fn new() -> Self {
SshConfig {
password: None,
username: None,
host: None,
project_dir: None,
}
}
pub fn load() -> Result<Self, String> {
let config_path = get_config_path();
let legacy_path = legacy_config_path();
let path_to_read = if config_path.exists() {
config_path
} else if legacy_path.exists() {
legacy_path
} else {
return Ok(SshConfig::new());
};
let content = fs::read_to_string(&path_to_read)
.map_err(|e| format!("Failed to read config file: {}", e))?;
serde_yaml::from_str(&content).map_err(|e| format!("Failed to parse config file: {}", e))
}
pub fn save(&self) -> Result<(), String> {
let config_path = get_config_path();
let config_dir = config_path.parent().ok_or("Invalid config path")?;
fs::create_dir_all(config_dir)
.map_err(|e| format!("Failed to create config directory: {}", e))?;
let content = serde_yaml::to_string(self)
.map_err(|e| format!("Failed to serialize config: {}", e))?;
fs::write(&config_path, content).map_err(|e| format!("Failed to write config file: {}", e))
}
}
pub fn ensure_global_xbp_paths() -> Result<GlobalXbpPaths, String> {
let root_dir = dirs::config_dir()
.or_else(|| dirs::home_dir().map(|home| home.join(".config")))
.unwrap_or_else(|| PathBuf::from("."))
.join("xbp");
let paths = GlobalXbpPaths {
config_file: root_dir.join("config.yaml"),
ssh_dir: root_dir.join("ssh"),
cache_dir: root_dir.join("cache"),
logs_dir: root_dir.join("logs"),
root_dir,
};
for dir in [
&paths.root_dir,
&paths.ssh_dir,
&paths.cache_dir,
&paths.logs_dir,
] {
fs::create_dir_all(dir)
.map_err(|e| format!("Failed to create XBP directory {}: {}", dir.display(), e))?;
}
if !paths.config_file.exists() {
fs::write(
&paths.config_file,
"password: null\nusername: null\nhost: null\nproject_dir: null\n",
)
.map_err(|e| {
format!(
"Failed to initialize config file {}: {}",
paths.config_file.display(),
e
)
})?;
}
Ok(paths)
}
pub fn global_xbp_paths() -> Result<GlobalXbpPaths, String> {
ensure_global_xbp_paths()
}
pub fn get_config_path() -> PathBuf {
ensure_global_xbp_paths()
.map(|paths| paths.config_file)
.unwrap_or_else(|_| legacy_config_path())
}
fn legacy_config_path() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".xbp")
.join("config.yaml")
}
pub fn describe_global_xbp_paths() -> Result<Vec<(String, PathBuf)>, String> {
let paths = global_xbp_paths()?;
Ok(vec![
("root".to_string(), paths.root_dir),
("config".to_string(), paths.config_file),
("ssh".to_string(), paths.ssh_dir),
("cache".to_string(), paths.cache_dir),
("logs".to_string(), paths.logs_dir),
])
}
const DEFAULT_API_XBP_URL: &str = "https://api.xbp.app";
#[derive(Debug, Clone)]
pub struct ApiConfig {
base_url: String,
}
impl ApiConfig {
pub fn load() -> Self {
let raw_url = env::var("API_XBP_URL").unwrap_or_else(|_| DEFAULT_API_XBP_URL.to_string());
let base_url = Self::normalize_base_url(&raw_url);
ApiConfig { base_url }
}
pub fn base_url(&self) -> &str {
&self.base_url
}
pub fn version_endpoint(&self, project_name: &str) -> String {
format!("{}/version?project_name={}", self.base_url, project_name)
}
pub fn increment_endpoint(&self) -> String {
format!("{}/version/increment", self.base_url)
}
fn normalize_base_url(raw: &str) -> String {
let trimmed = raw.trim();
if trimmed.is_empty() {
return DEFAULT_API_XBP_URL.to_string();
}
let trimmed = trimmed.trim_end_matches('/');
if trimmed.is_empty() {
return DEFAULT_API_XBP_URL.to_string();
}
trimmed.to_string()
}
}