use crate::error::{ProxyError, Result};
use directories::ProjectDirs;
use std::path::{Path, PathBuf};
const APP_NAME: &str = "modelmux";
const ORGANIZATION: &str = "com";
const ORG_NAME: &str = "SkyCorp";
pub fn user_config_dir() -> Result<PathBuf> {
let project_dirs = get_project_dirs()?;
let config_dir = project_dirs.config_dir();
ensure_directory_exists(config_dir)?;
Ok(config_dir.to_path_buf())
}
#[allow(dead_code)]
pub fn user_data_dir() -> Result<PathBuf> {
let project_dirs = get_project_dirs()?;
let data_dir = project_dirs.data_dir();
ensure_directory_exists(data_dir)?;
Ok(data_dir.to_path_buf())
}
#[allow(dead_code)]
pub fn user_cache_dir() -> Result<PathBuf> {
let project_dirs = get_project_dirs()?;
let cache_dir = project_dirs.cache_dir();
ensure_directory_exists(cache_dir)?;
Ok(cache_dir.to_path_buf())
}
pub fn system_config_dir() -> Result<PathBuf> {
#[cfg(all(unix, not(target_os = "macos")))]
{
Ok(PathBuf::from("/etc").join(APP_NAME))
}
#[cfg(target_os = "macos")]
{
Ok(PathBuf::from("/Library/Preferences").join(APP_NAME))
}
#[cfg(windows)]
{
std::env::var("PROGRAMDATA").map(|path| PathBuf::from(path).join(APP_NAME)).map_err(|_| {
ProxyError::Config("PROGRAMDATA environment variable not found".to_string())
})
}
}
pub fn user_config_file() -> Result<PathBuf> {
Ok(user_config_dir()?.join("config.toml"))
}
pub fn system_config_file() -> Result<PathBuf> {
Ok(system_config_dir()?.join("config.toml"))
}
pub fn default_service_account_file() -> Result<PathBuf> {
Ok(user_config_dir()?.join("service-account.json"))
}
pub fn expand_path<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
let path_str = path.as_ref().to_string_lossy();
if path_str.starts_with("~/") {
if let Some(dirs) = directories::UserDirs::new() {
let expanded = dirs.home_dir().join(&path_str[2..]);
return Ok(expanded);
} else {
return Err(ProxyError::Config(
"Unable to determine user home directory for tilde expansion".to_string(),
));
}
}
if path_str.contains('$') {
let expanded = shellexpand::full(&path_str).map_err(|e| {
ProxyError::Config(format!(
"Failed to expand environment variables in path '{}': {}",
path_str, e
))
})?;
return Ok(PathBuf::from(expanded.as_ref()));
}
Ok(path.as_ref().to_path_buf())
}
pub fn validate_config_file<P: AsRef<Path>>(path: P) -> Result<()> {
let path = path.as_ref();
if !path.exists() {
return Err(ProxyError::Config(format!(
"Configuration file '{}' does not exist",
path.display()
)));
}
if !path.is_file() {
return Err(ProxyError::Config(format!(
"Configuration path '{}' exists but is not a regular file",
path.display()
)));
}
std::fs::File::open(path).map_err(|e| {
ProxyError::Config(format!(
"Configuration file '{}' exists but cannot be read: {}\n\
\n\
Please check file permissions. The file should be readable by the current user.\n\
You can fix this with: chmod 644 '{}'",
path.display(),
e,
path.display()
))
})?;
Ok(())
}
pub fn config_file_paths() -> Vec<PathBuf> {
let mut paths = Vec::new();
if let Ok(user_config) = user_config_file() {
paths.push(user_config);
}
if let Ok(system_config) = system_config_file() {
paths.push(system_config);
}
paths
}
fn get_project_dirs() -> Result<ProjectDirs> {
ProjectDirs::from(ORGANIZATION, ORG_NAME, APP_NAME).ok_or_else(|| {
ProxyError::Config(
"Unable to determine user directories. This may indicate:\n\
1. No valid home directory found\n\
2. Platform-specific directory resolution failed\n\
3. Insufficient permissions to access user directories\n\
\n\
Please ensure your user account has a valid home directory."
.to_string(),
)
})
}
fn ensure_directory_exists<P: AsRef<Path>>(path: P) -> Result<()> {
let path = path.as_ref();
if path.exists() {
if !path.is_dir() {
return Err(ProxyError::Config(format!(
"Path '{}' exists but is not a directory",
path.display()
)));
}
return Ok(());
}
std::fs::create_dir_all(path).map_err(|e| {
ProxyError::Config(format!(
"Failed to create configuration directory '{}': {}\n\
\n\
Please ensure:\n\
1. You have write permissions to the parent directory\n\
2. There's sufficient disk space\n\
3. No conflicting files exist in the path",
path.display(),
e
))
})?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_user_config_dir_creation() {
let config_dir = user_config_dir().expect("Should get user config directory");
assert!(config_dir.exists(), "Config directory should be created");
assert!(config_dir.is_dir(), "Config path should be a directory");
}
#[test]
fn test_user_config_file_path() {
let config_file = user_config_file().expect("Should get config file path");
assert!(config_file.file_name().unwrap() == "config.toml");
assert!(config_file.parent().unwrap().exists(), "Parent directory should exist");
}
#[test]
fn test_tilde_expansion() {
let expanded = expand_path("~/test/path").expect("Should expand tilde");
assert!(!expanded.to_string_lossy().contains('~'), "Tilde should be expanded");
let absolute = expand_path("/absolute/path").expect("Should handle absolute path");
assert_eq!(absolute, PathBuf::from("/absolute/path"));
}
#[test]
fn test_validate_config_file() {
let result = validate_config_file("/non/existent/file.toml");
assert!(result.is_err());
let temp_dir = TempDir::new().unwrap();
let temp_file = temp_dir.path().join("test.toml");
fs::write(&temp_file, "test content").unwrap();
let result = validate_config_file(&temp_file);
assert!(result.is_ok());
}
#[test]
fn test_config_file_paths_order() {
let paths = config_file_paths();
assert!(!paths.is_empty(), "Should return at least one config path");
if paths.len() > 1 {
let user_path = &paths[0];
let system_path = &paths[1];
assert!(
user_path.to_string_lossy().contains("config"),
"First path should be user config"
);
#[cfg(unix)]
assert!(system_path.starts_with("/etc") || system_path.starts_with("/Library"));
}
}
#[test]
fn test_default_service_account_file() {
let sa_file = default_service_account_file().expect("Should get service account path");
assert!(sa_file.file_name().unwrap() == "service-account.json");
assert!(sa_file.parent().unwrap().exists(), "Parent directory should exist");
}
}