doum_cli/system/
config.rs1use crate::system::error::{DoumError, Result};
2use crate::system::paths::get_config_path;
3use crate::llm::{OpenAIConfig, AnthropicConfig};
4use rust_embed::RustEmbed;
5use serde::{Deserialize, Serialize};
6use std::fs;
7use std::path::PathBuf;
8use std::collections::HashMap;
9
10#[cfg(unix)]
11use std::os::unix::fs::PermissionsExt;
12
13#[derive(RustEmbed)]
15#[folder = "static/"]
16struct StaticAssets;
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct Config {
21 pub llm: LLMConfig,
22 pub logging: LoggingConfig,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct LLMConfig {
28 pub provider: String,
29 pub providers: HashMap<String, ProviderConfig>,
30 pub context: ContextConfig,
31 pub timeout: u64,
32 pub max_retries: u32,
33 pub use_thinking: bool,
34 pub use_web_search: bool,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39#[serde(tag = "type", rename_all = "lowercase")]
40pub enum ProviderConfig {
41 Openai(OpenAIConfig),
42 Anthropic(AnthropicConfig),
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct ContextConfig {
48 pub max_lines: usize,
49 pub max_size_kb: usize,
50}
51
52impl LLMConfig {
53 pub fn get_current_provider(&self) -> Result<&ProviderConfig> {
55 self.providers
56 .get(&self.provider)
57 .ok_or_else(|| DoumError::Config(
58 format!("프로바이더 '{}'를 찾을 수 없습니다", self.provider)
59 ))
60 }
61
62 pub fn get_provider(&self, name: &str) -> Result<&ProviderConfig> {
64 self.providers
65 .get(name)
66 .ok_or_else(|| DoumError::Config(
67 format!("프로바이더 '{}'를 찾을 수 없습니다", name)
68 ))
69 }
70
71 pub fn get_provider_mut(&mut self, name: &str) -> Result<&mut ProviderConfig> {
73 self.providers
74 .get_mut(name)
75 .ok_or_else(|| DoumError::Config(
76 format!("프로바이더 '{}'를 찾을 수 없습니다", name)
77 ))
78 }
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct LoggingConfig {
84 pub enabled: bool,
85 pub level: String,
86}
87
88fn ensure_config() -> Result<PathBuf> {
90 let config_path = get_config_path()?;
91
92 if let Some(parent) = config_path.parent()
93 && !parent.exists() {
94 fs::create_dir_all(parent)
95 .map_err(|e| DoumError::Config(format!("설정 디렉터리 생성 실패: {}", e)))?;
96
97 #[cfg(unix)]
99 {
100 let metadata = fs::metadata(parent)
101 .map_err(|e| DoumError::Config(format!("디렉터리 메타데이터 읽기 실패: {}", e)))?;
102 let mut permissions = metadata.permissions();
103 permissions.set_mode(0o700);
104 fs::set_permissions(parent, permissions)
105 .map_err(|e| DoumError::Config(format!("디렉터리 권한 설정 실패: {}", e)))?;
106 }
107 }
108
109 Ok(config_path)
110}
111
112pub fn load_config() -> Result<Config> {
114 let config_path = ensure_config()?;
115
116 if config_path.exists() {
117 validate_config(&config_path)?;
119
120 let content = fs::read_to_string(&config_path)
122 .map_err(|e| DoumError::Config(format!("설정 파일 읽기 실패: {}", e)))?;
123
124 let config: Config = toml::from_str(&content)
126 .map_err(|e| DoumError::Config(format!("설정 파일 파싱 실패: {}", e)))?;
127
128 Ok(config)
129 } else {
130 let config = load_default_config()?;
132 save_config(&config)?;
133 Ok(config)
134 }
135}
136
137pub fn load_default_config() -> Result<Config> {
139 let config_content = StaticAssets::get("config.toml")
140 .ok_or_else(|| DoumError::Config("기본 설정 파일을 찾을 수 없습니다".to_string()))?;
141
142 let config_str = std::str::from_utf8(config_content.data.as_ref())
143 .map_err(|e| DoumError::Config(format!("기본 설정 파일 인코딩 실패: {}", e)))?;
144
145 let config: Config = toml::from_str(config_str)
146 .map_err(|e| DoumError::Config(format!("기본 설정 파싱 실패: {}", e)))?;
147
148 Ok(config)
149}
150
151pub fn save_config(config: &Config) -> Result<()> {
153 let config_path = ensure_config()?;
154
155 let content = toml::to_string_pretty(config)
157 .map_err(|e| DoumError::Config(format!("설정 직렬화 실패: {}", e)))?;
158
159 fs::write(&config_path, content)
161 .map_err(|e| DoumError::Config(format!("설정 파일 쓰기 실패: {}", e)))?;
162
163 #[cfg(windows)]
165 {
166 }
169
170 #[cfg(unix)]
172 {
173 let metadata = fs::metadata(&config_path)
174 .map_err(|e| DoumError::Config(format!("파일 메타데이터 읽기 실패: {}", e)))?;
175 let mut permissions = metadata.permissions();
176 permissions.set_mode(0o600);
177 fs::set_permissions(&config_path, permissions)
178 .map_err(|e| DoumError::Config(format!("파일 권한 설정 실패: {}", e)))?;
179 }
180
181 Ok(())
182}
183
184fn validate_config(path: &PathBuf) -> Result<()> {
186 #[cfg(windows)]
187 {
188 let _ = path; }
192
193 #[cfg(unix)]
194 {
195 let metadata = fs::metadata(path)
196 .map_err(|e| DoumError::Config(format!("파일 메타데이터 읽기 실패: {}", e)))?;
197 let permissions = metadata.permissions();
198 let mode = permissions.mode() & 0o777;
199
200 if mode != 0o600 && mode != 0o400 {
202 return Err(DoumError::InvalidConfig(
203 format!(
204 "설정 파일 권한이 안전하지 않습니다 (현재: {:o}, 필요: 600 또는 400). \
205 다음 명령으로 수정하세요: chmod 600 {}",
206 mode,
207 path.display()
208 )
209 ));
210 }
211 }
212
213 Ok(())
214}