use crate::core::file_error::LARGE_FILE_SIZE;
use crate::upgrade::config::UpgradeConfig;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use tokio::fs;
const fn default_max_content_file_size() -> u64 {
LARGE_FILE_SIZE as u64
}
const fn default_token_warning_threshold() -> u64 {
100_000
}
fn is_default_token_warning_threshold(value: &u64) -> bool {
*value == default_token_warning_threshold()
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct GlobalConfig {
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub sources: HashMap<String, String>,
#[serde(default, skip_serializing_if = "is_default_upgrade_config")]
pub upgrade: UpgradeConfig,
#[serde(
default = "default_max_content_file_size",
skip_serializing_if = "is_default_max_content_file_size"
)]
pub max_content_file_size: u64,
#[serde(
default = "default_token_warning_threshold",
skip_serializing_if = "is_default_token_warning_threshold"
)]
pub token_warning_threshold: u64,
}
fn is_default_max_content_file_size(size: &u64) -> bool {
*size == default_max_content_file_size()
}
const fn is_default_upgrade_config(config: &UpgradeConfig) -> bool {
!config.check_on_startup
&& config.check_interval == 86400
&& config.auto_backup
&& config.verify_checksum
}
impl GlobalConfig {
pub async fn load() -> Result<Self> {
let path = Self::default_path()?;
if path.exists() {
Self::load_from(&path).await
} else {
Ok(Self::default())
}
}
pub async fn load_with_optional(path: Option<PathBuf>) -> Result<Self> {
let path = path.unwrap_or_else(|| {
Self::default_path().unwrap_or_else(|_| PathBuf::from("~/.agpm/config.toml"))
});
if path.exists() {
Self::load_from(&path).await
} else {
Ok(Self::default())
}
}
pub async fn load_from(path: &Path) -> Result<Self> {
let content = fs::read_to_string(path)
.await
.with_context(|| format!("Failed to read global config from {}", path.display()))?;
toml::from_str(&content)
.with_context(|| format!("Failed to parse global config from {}", path.display()))
}
pub async fn save(&self) -> Result<()> {
let path = Self::default_path()?;
self.save_to(&path).await
}
pub async fn save_to(&self, path: &Path) -> Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).await.with_context(|| {
format!("Failed to create config directory: {}", parent.display())
})?;
}
let content = toml::to_string_pretty(self).context("Failed to serialize global config")?;
fs::write(path, content)
.await
.with_context(|| format!("Failed to write global config to {}", path.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
use tokio::fs as async_fs;
let mut perms = async_fs::metadata(path)
.await
.with_context(|| format!("Failed to read permissions for {}", path.display()))?
.permissions();
perms.set_mode(0o600); async_fs::set_permissions(path, perms).await.with_context(|| {
format!("Failed to set secure permissions on {}", path.display())
})?;
}
Ok(())
}
pub fn default_path() -> Result<PathBuf> {
let config_dir = if cfg!(target_os = "windows") {
dirs::data_local_dir()
.ok_or_else(|| anyhow::anyhow!("Unable to determine local data directory"))?
.join("agpm")
} else {
dirs::home_dir()
.ok_or_else(|| anyhow::anyhow!("Unable to determine home directory"))?
.join(".agpm")
};
Ok(config_dir.join("config.toml"))
}
#[must_use]
pub fn merge_sources(
&self,
local_sources: &HashMap<String, String>,
) -> HashMap<String, String> {
let mut merged = self.sources.clone();
for (name, url) in local_sources {
merged.insert(name.clone(), url.clone());
}
merged
}
pub fn add_source(&mut self, name: String, url: String) {
self.sources.insert(name, url);
}
pub fn remove_source(&mut self, name: &str) -> bool {
self.sources.remove(name).is_some()
}
#[must_use]
pub fn has_source(&self, name: &str) -> bool {
self.sources.contains_key(name)
}
#[must_use]
pub fn get_source(&self, name: &str) -> Option<&String> {
self.sources.get(name)
}
#[must_use]
pub fn init_example() -> Self {
let mut sources = HashMap::new();
sources.insert(
"private".to_string(),
"https://oauth2:YOUR_TOKEN@github.com/yourcompany/private-agpm.git".to_string(),
);
Self {
sources,
upgrade: UpgradeConfig::default(),
max_content_file_size: default_max_content_file_size(),
token_warning_threshold: default_token_warning_threshold(),
}
}
}
pub struct GlobalConfigManager {
config: Option<GlobalConfig>,
path: PathBuf,
}
impl GlobalConfigManager {
pub fn new() -> Result<Self> {
Ok(Self {
config: None,
path: GlobalConfig::default_path()?,
})
}
#[must_use]
pub const fn with_path(path: PathBuf) -> Self {
Self {
config: None,
path,
}
}
pub async fn get(&mut self) -> Result<&GlobalConfig> {
if self.config.is_none() {
self.config = Some(if self.path.exists() {
GlobalConfig::load_from(&self.path).await?
} else {
GlobalConfig::default()
});
}
Ok(self.config.as_ref().unwrap())
}
pub async fn get_mut(&mut self) -> Result<&mut GlobalConfig> {
if self.config.is_none() {
self.config = Some(if self.path.exists() {
GlobalConfig::load_from(&self.path).await?
} else {
GlobalConfig::default()
});
}
Ok(self.config.as_mut().unwrap())
}
pub async fn save(&self) -> Result<()> {
if let Some(config) = &self.config {
config.save_to(&self.path).await?;
}
Ok(())
}
pub async fn reload(&mut self) -> Result<()> {
self.config = Some(if self.path.exists() {
GlobalConfig::load_from(&self.path).await?
} else {
GlobalConfig::default()
});
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[tokio::test]
async fn test_global_config_default() {
let config = GlobalConfig::default();
assert!(config.sources.is_empty());
}
#[tokio::test]
async fn test_global_config_save_load() {
let temp = TempDir::new().unwrap();
let config_path = temp.path().join("config.toml");
let mut config = GlobalConfig::default();
config.add_source("test".to_string(), "https://example.com/repo.git".to_string());
config.save_to(&config_path).await.unwrap();
let loaded = GlobalConfig::load_from(&config_path).await.unwrap();
assert_eq!(loaded.sources.len(), 1);
assert_eq!(loaded.get_source("test"), Some(&"https://example.com/repo.git".to_string()));
}
#[tokio::test]
async fn test_merge_sources() {
let mut global = GlobalConfig::default();
global.add_source("private".to_string(), "https://token@private.com/repo.git".to_string());
global.add_source("shared".to_string(), "https://shared.com/repo.git".to_string());
let mut local = HashMap::new();
local.insert("shared".to_string(), "https://override.com/repo.git".to_string());
local.insert("public".to_string(), "https://public.com/repo.git".to_string());
let merged = global.merge_sources(&local);
assert_eq!(merged.get("private"), Some(&"https://token@private.com/repo.git".to_string()));
assert_eq!(merged.get("shared"), Some(&"https://override.com/repo.git".to_string()));
assert_eq!(merged.get("public"), Some(&"https://public.com/repo.git".to_string()));
}
#[tokio::test]
async fn test_source_operations() {
let mut config = GlobalConfig::default();
config.add_source("test".to_string(), "https://test.com/repo.git".to_string());
assert!(config.has_source("test"));
assert_eq!(config.get_source("test"), Some(&"https://test.com/repo.git".to_string()));
config.add_source("test".to_string(), "https://updated.com/repo.git".to_string());
assert_eq!(config.get_source("test"), Some(&"https://updated.com/repo.git".to_string()));
assert!(config.remove_source("test"));
assert!(!config.has_source("test"));
assert!(!config.remove_source("test")); }
#[tokio::test]
async fn test_init_example() {
let config = GlobalConfig::init_example();
assert!(config.has_source("private"));
assert_eq!(
config.get_source("private"),
Some(&"https://oauth2:YOUR_TOKEN@github.com/yourcompany/private-agpm.git".to_string())
);
}
#[tokio::test]
#[cfg(unix)]
async fn test_config_file_permissions() {
use std::os::unix::fs::PermissionsExt;
let temp_dir = tempfile::tempdir().unwrap();
let config_path = temp_dir.path().join("test-config.toml");
let config = GlobalConfig::default();
config.save_to(&config_path).await.unwrap();
let metadata = tokio::fs::metadata(&config_path).await.unwrap();
let permissions = metadata.permissions();
let mode = permissions.mode() & 0o777;
assert_eq!(mode, 0o600, "Config file should have 600 permissions");
}
#[tokio::test]
async fn test_config_manager() {
let temp = TempDir::new().unwrap();
let config_path = temp.path().join("config.toml");
let mut manager = GlobalConfigManager::with_path(config_path.clone());
let config = manager.get_mut().await.unwrap();
config.add_source("test".to_string(), "https://test.com/repo.git".to_string());
manager.save().await.unwrap();
let mut manager2 = GlobalConfigManager::with_path(config_path);
let config2 = manager2.get().await.unwrap();
assert!(config2.has_source("test"));
}
}