alopex_cli/profile/
config.rs1use std::collections::HashMap;
2use std::fs::{self, OpenOptions};
3use std::io::Write;
4use std::path::PathBuf;
5
6use serde::{Deserialize, Serialize};
7
8use crate::cli::Cli;
9use crate::error::{CliError, Result};
10
11#[cfg(unix)]
12use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
13
14const CONFIG_DIR: &str = ".alopex";
15const CONFIG_FILE: &str = "config.toml";
16
17#[derive(Debug, Serialize, Deserialize, Default)]
18pub struct ProfileConfig {
19 pub default: Option<String>,
20 #[serde(default)]
21 pub profiles: HashMap<String, Profile>,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct Profile {
26 pub data_dir: String,
27}
28
29#[derive(Debug)]
30pub struct ResolvedConfig {
31 pub data_dir: Option<String>,
32 pub in_memory: bool,
33 #[allow(dead_code)]
34 pub profile_name: Option<String>,
35}
36
37#[derive(Debug)]
38pub struct ProfileManager {
39 config_path: PathBuf,
40 profiles: HashMap<String, Profile>,
41 default_profile: Option<String>,
42}
43
44impl ProfileManager {
45 pub fn load() -> Result<Self> {
46 let config_path = default_config_path()?;
47 Self::load_from_path(config_path)
48 }
49
50 pub fn load_from_path(config_path: PathBuf) -> Result<Self> {
51 let config = if config_path.exists() {
52 let contents = fs::read_to_string(&config_path)?;
53 if contents.trim().is_empty() {
54 ProfileConfig::default()
55 } else {
56 toml::from_str::<ProfileConfig>(&contents)
57 .map_err(|err| CliError::Parse(err.to_string()))?
58 }
59 } else {
60 ProfileConfig::default()
61 };
62
63 Ok(Self {
64 config_path,
65 profiles: config.profiles,
66 default_profile: config.default,
67 })
68 }
69
70 pub fn save(&self) -> Result<()> {
71 if let Some(parent) = self.config_path.parent() {
72 fs::create_dir_all(parent)?;
73 }
74
75 let config = ProfileConfig {
76 default: self.default_profile.clone(),
77 profiles: self.profiles.clone(),
78 };
79 let serialized =
80 toml::to_string_pretty(&config).map_err(|err| CliError::Parse(err.to_string()))?;
81
82 let mut options = OpenOptions::new();
83 options.write(true).create(true).truncate(true);
84 #[cfg(unix)]
85 {
86 options.mode(0o600);
87 }
88 let mut file = options.open(&self.config_path)?;
89 file.write_all(serialized.as_bytes())?;
90 file.flush()?;
91
92 #[cfg(unix)]
93 fs::set_permissions(&self.config_path, fs::Permissions::from_mode(0o600))?;
94
95 Ok(())
96 }
97
98 pub fn create(&mut self, name: &str, profile: Profile) -> Result<()> {
99 self.profiles.insert(name.to_string(), profile);
100 Ok(())
101 }
102
103 pub fn delete(&mut self, name: &str) -> Result<()> {
104 if self.profiles.remove(name).is_none() {
105 return Err(CliError::ProfileNotFound(name.to_string()));
106 }
107
108 if self.default_profile.as_deref() == Some(name) {
109 self.default_profile = None;
110 }
111
112 Ok(())
113 }
114
115 pub fn get(&self, name: &str) -> Option<&Profile> {
116 self.profiles.get(name)
117 }
118
119 pub fn list(&self) -> Vec<&str> {
120 let mut names: Vec<&str> = self.profiles.keys().map(|name| name.as_str()).collect();
121 names.sort_unstable();
122 names
123 }
124
125 pub fn set_default(&mut self, name: &str) -> Result<()> {
126 if !self.profiles.contains_key(name) {
127 return Err(CliError::ProfileNotFound(name.to_string()));
128 }
129
130 self.default_profile = Some(name.to_string());
131 Ok(())
132 }
133
134 pub fn default_profile(&self) -> Option<&str> {
135 self.default_profile.as_deref()
136 }
137
138 pub fn resolve(&self, cli: &Cli) -> Result<ResolvedConfig> {
139 if cli.profile.is_some() && cli.data_dir.is_some() {
140 return Err(CliError::ConflictingOptions);
141 }
142
143 if let Some(profile_name) = cli.profile.as_deref() {
144 let profile = self
145 .profiles
146 .get(profile_name)
147 .ok_or_else(|| CliError::ProfileNotFound(profile_name.to_string()))?;
148 return Ok(ResolvedConfig {
149 data_dir: Some(profile.data_dir.clone()),
150 in_memory: false,
151 profile_name: Some(profile_name.to_string()),
152 });
153 }
154
155 if let Some(data_dir) = cli.data_dir.as_ref() {
156 return Ok(ResolvedConfig {
157 data_dir: Some(data_dir.clone()),
158 in_memory: false,
159 profile_name: None,
160 });
161 }
162
163 if let Some(default_name) = self.default_profile.as_deref() {
164 let profile = self
165 .profiles
166 .get(default_name)
167 .ok_or_else(|| CliError::ProfileNotFound(default_name.to_string()))?;
168 return Ok(ResolvedConfig {
169 data_dir: Some(profile.data_dir.clone()),
170 in_memory: false,
171 profile_name: Some(default_name.to_string()),
172 });
173 }
174
175 Ok(ResolvedConfig {
176 data_dir: None,
177 in_memory: true,
178 profile_name: None,
179 })
180 }
181}
182
183fn default_config_path() -> Result<PathBuf> {
184 let home = dirs::home_dir().ok_or_else(|| {
185 CliError::InvalidArgument("Home directory could not be determined".to_string())
186 })?;
187 Ok(home.join(CONFIG_DIR).join(CONFIG_FILE))
188}