doum_cli/system/
config.rs

1use 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/// 정적 파일 임베딩
14#[derive(RustEmbed)]
15#[folder = "static/"]
16struct StaticAssets;
17
18/// 전체 설정 구조
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct Config {
21    pub llm: LLMConfig,
22    pub logging: LoggingConfig,
23}
24
25/// LLM 관련 설정
26#[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/// 프로바이더별 설정
38#[derive(Debug, Clone, Serialize, Deserialize)]
39#[serde(tag = "type", rename_all = "lowercase")]
40pub enum ProviderConfig {
41    Openai(OpenAIConfig),
42    Anthropic(AnthropicConfig),
43}
44
45/// 컨텍스트 수집 설정
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct ContextConfig {
48    pub max_lines: usize,
49    pub max_size_kb: usize,
50}
51
52impl LLMConfig {
53    /// 현재 선택된 프로바이더 설정 가져오기
54    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    /// 특정 프로바이더 설정 가져오기
63    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    /// 특정 프로바이더 설정 mutable 참조 가져오기
72    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/// 로깅 설정
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct LoggingConfig {
84    pub enabled: bool,
85    pub level: String,
86}
87
88/// 설정 디렉터리 생성 및 권한 설정
89fn 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            // Unix 시스템에서 디렉터리 권한 설정 (700)
98            #[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
112/// 설정 파일 로드 (없으면 기본값으로 생성)
113pub fn load_config() -> Result<Config> {
114    let config_path = ensure_config()?;
115    
116    if config_path.exists() {
117        // 권한 검증
118        validate_config(&config_path)?;
119        
120        // 설정 파일 읽기
121        let content = fs::read_to_string(&config_path)
122            .map_err(|e| DoumError::Config(format!("설정 파일 읽기 실패: {}", e)))?;
123        
124        // TOML 파싱
125        let config: Config = toml::from_str(&content)
126            .map_err(|e| DoumError::Config(format!("설정 파일 파싱 실패: {}", e)))?;
127        
128        Ok(config)
129    } else {
130        // 임베드된 기본 config.toml을 로드하여 저장
131        let config = load_default_config()?;
132        save_config(&config)?;
133        Ok(config)
134    }
135}
136
137/// 임베드된 기본 config.toml 로드
138pub 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
151/// 설정 파일 저장
152pub fn save_config(config: &Config) -> Result<()> {
153    let config_path = ensure_config()?;
154    
155    // TOML로 직렬화
156    let content = toml::to_string_pretty(config)
157        .map_err(|e| DoumError::Config(format!("설정 직렬화 실패: {}", e)))?;
158    
159    // 파일 쓰기
160    fs::write(&config_path, content)
161        .map_err(|e| DoumError::Config(format!("설정 파일 쓰기 실패: {}", e)))?;
162    
163    // Windows에서는 기본 ACL 사용
164    #[cfg(windows)]
165    {
166        // Windows의 경우 기본 ACL이 이미 적절하게 설정되어 있음
167        // 추가 보안이 필요한 경우 winapi를 사용하여 ACL 설정 가능
168    }
169
170    // Unix에서 파일 권한 설정 (600)
171    #[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
184/// 설정 파일 권한 검증
185fn validate_config(path: &PathBuf) -> Result<()> {
186    #[cfg(windows)]
187    {
188        // Windows에서는 기본적으로 안전하다고 가정
189        // 추가 검증이 필요한 경우 구현 가능
190        let _ = path; // unused warning 방지
191    }
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        // 600 또는 400 권한만 허용
201        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}