1use super::provider::ProviderService;
2use crate::app_config::{AppType, MultiAppConfig};
3use crate::database::Database;
4use crate::error::AppError;
5use crate::provider::Provider;
6use crate::store::AppState;
7use chrono::Utc;
8use serde_json::Value;
9use std::fs;
10use std::path::{Path, PathBuf};
11
12const MAX_BACKUPS: usize = 10;
13
14#[derive(Debug, Clone)]
16pub struct BackupInfo {
17 pub id: String,
19 pub path: PathBuf,
21 pub timestamp: String,
23 pub display_name: String,
25}
26
27pub struct ConfigService;
29
30impl ConfigService {
31 pub fn create_backup(
41 config_path: &Path,
42 custom_name: Option<String>,
43 ) -> Result<String, AppError> {
44 let db_path = crate::config::get_app_config_dir().join("cc-switch.db");
45 if !db_path.exists() {
46 return Ok(String::new());
47 }
48
49 let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
50 let backup_id = if let Some(name) = custom_name {
51 format!("{}_{}", name, timestamp)
52 } else {
53 format!("backup_{}", timestamp)
54 };
55
56 let backup_dir = config_path
57 .parent()
58 .or_else(|| db_path.parent())
59 .ok_or_else(|| AppError::Config("Invalid config path".into()))?
60 .join("backups");
61
62 fs::create_dir_all(&backup_dir).map_err(|e| AppError::io(&backup_dir, e))?;
63
64 let backup_path = backup_dir.join(format!("{backup_id}.sql"));
65 let db = Database::init()?;
66 db.export_sql(&backup_path)?;
67
68 Self::cleanup_old_backups(&backup_dir, MAX_BACKUPS)?;
69
70 Ok(backup_id)
71 }
72
73 pub fn list_backups(config_path: &Path) -> Result<Vec<BackupInfo>, AppError> {
75 let backup_dir = config_path
76 .parent()
77 .ok_or_else(|| AppError::Config("Invalid config path".into()))?
78 .join("backups");
79
80 if !backup_dir.exists() {
81 return Ok(Vec::new());
82 }
83
84 let entries = fs::read_dir(&backup_dir).map_err(|e| AppError::io(&backup_dir, e))?;
85
86 let mut backups: Vec<BackupInfo> = entries
87 .filter_map(|entry| entry.ok())
88 .filter(|entry| {
89 entry
90 .path()
91 .extension()
92 .map(|ext| ext == "sql")
93 .unwrap_or(false)
94 })
95 .filter_map(|entry| {
96 let path = entry.path();
97 let filename = path.file_stem()?.to_str()?.to_string();
98
99 let timestamp = Self::extract_timestamp(&filename)?;
101
102 let display_name = Self::format_display_name(&filename, ×tamp);
104
105 Some(BackupInfo {
106 id: filename.clone(),
107 path: path.clone(),
108 timestamp,
109 display_name,
110 })
111 })
112 .collect();
113
114 backups.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
116
117 Ok(backups)
118 }
119
120 pub fn restore_from_backup_id(backup_id: &str, state: &AppState) -> Result<String, AppError> {
122 let config_path = crate::config::get_app_config_path();
123 let backup_dir = config_path
124 .parent()
125 .ok_or_else(|| AppError::Config("Invalid config path".into()))?
126 .join("backups");
127
128 let backup_path = backup_dir.join(format!("{}.sql", backup_id));
129
130 if !backup_path.exists() {
131 return Err(AppError::Message(format!("备份文件不存在: {}", backup_id)));
132 }
133
134 Self::import_config_from_path(&backup_path, state)
135 }
136
137 fn extract_timestamp(filename: &str) -> Option<String> {
139 let parts: Vec<&str> = filename.rsplitn(3, '_').collect();
141 if parts.len() >= 2 {
142 Some(format!("{}_{}", parts[1], parts[0]))
144 } else {
145 None
146 }
147 }
148
149 fn format_display_name(filename: &str, timestamp: &str) -> String {
151 if timestamp.len() == 15 {
153 let date = ×tamp[0..8];
155 let time = ×tamp[9..15];
156
157 if let (Ok(y), Ok(m), Ok(d), Ok(h), Ok(min), Ok(s)) = (
158 date[0..4].parse::<u32>(),
159 date[4..6].parse::<u32>(),
160 date[6..8].parse::<u32>(),
161 time[0..2].parse::<u32>(),
162 time[2..4].parse::<u32>(),
163 time[4..6].parse::<u32>(),
164 ) {
165 let formatted_time =
166 format!("{:04}-{:02}-{:02} {:02}:{:02}:{:02}", y, m, d, h, min, s);
167
168 if !filename.starts_with("backup_") {
170 let custom_name = filename.rsplitn(3, '_').nth(2).unwrap_or(filename);
171 return format!("{} ({})", custom_name, formatted_time);
172 }
173
174 return formatted_time;
175 }
176 }
177
178 filename.to_string()
180 }
181
182 fn cleanup_old_backups(backup_dir: &Path, retain: usize) -> Result<(), AppError> {
183 if retain == 0 {
184 return Ok(());
185 }
186
187 let entries = match fs::read_dir(backup_dir) {
188 Ok(iter) => iter
189 .filter_map(|entry| entry.ok())
190 .filter(|entry| {
191 entry
192 .path()
193 .extension()
194 .map(|ext| ext == "sql")
195 .unwrap_or(false)
196 })
197 .collect::<Vec<_>>(),
198 Err(_) => return Ok(()),
199 };
200
201 if entries.len() <= retain {
202 return Ok(());
203 }
204
205 let remove_count = entries.len().saturating_sub(retain);
206 let mut sorted = entries;
207
208 sorted.sort_by(|a, b| {
209 let a_time = a.metadata().and_then(|m| m.modified()).ok();
210 let b_time = b.metadata().and_then(|m| m.modified()).ok();
211 a_time.cmp(&b_time)
212 });
213
214 for entry in sorted.into_iter().take(remove_count) {
215 if let Err(err) = fs::remove_file(entry.path()) {
216 log::warn!(
217 "Failed to remove old backup {}: {}",
218 entry.path().display(),
219 err
220 );
221 }
222 }
223
224 Ok(())
225 }
226
227 pub fn export_config_to_path(target_path: &Path) -> Result<(), AppError> {
229 let db = Database::init()?;
230 db.export_sql(target_path)
231 }
232
233 pub fn import_config_from_path(file_path: &Path, state: &AppState) -> Result<String, AppError> {
234 let db_path = crate::config::get_app_config_dir().join("cc-switch.db");
235 if !db_path.exists() {
236 return Err(AppError::Config("数据库不存在,无法导入".to_string()));
237 }
238
239 let backup_id = Self::create_backup(&db_path, None)?;
241
242 state.db.import_sql(file_path)?;
244 state.refresh_config_from_db()?;
245
246 Ok(backup_id)
247 }
248
249 pub fn sync_current_providers_to_live(config: &mut MultiAppConfig) -> Result<(), AppError> {
251 Self::sync_current_provider_for_app(config, &AppType::Claude)?;
252 Self::sync_current_provider_for_app(config, &AppType::Codex)?;
253 Self::sync_current_provider_for_app(config, &AppType::Gemini)?;
254 Self::sync_current_provider_for_app(config, &AppType::OpenCode)?;
255 Self::sync_current_provider_for_app(config, &AppType::OpenClaw)?;
256 Ok(())
257 }
258
259 fn sync_current_provider_for_app(
260 config: &mut MultiAppConfig,
261 app_type: &AppType,
262 ) -> Result<(), AppError> {
263 let (current_id, provider) = {
264 let manager = match config.get_manager(app_type) {
265 Some(manager) => manager,
266 None => return Ok(()),
267 };
268
269 if manager.current.is_empty() {
270 return Ok(());
271 }
272
273 let current_id = manager.current.clone();
274 let provider = match manager.providers.get(¤t_id) {
275 Some(provider) => provider.clone(),
276 None => {
277 log::warn!(
278 "当前应用 {app_type:?} 的供应商 {current_id} 不存在,跳过 live 同步"
279 );
280 return Ok(());
281 }
282 };
283 (current_id, provider)
284 };
285
286 match app_type {
287 AppType::Codex => Self::sync_codex_live(config, ¤t_id, &provider)?,
288 AppType::Claude => Self::sync_claude_live(config, ¤t_id, &provider)?,
289 AppType::Gemini => Self::sync_gemini_live(config, ¤t_id, &provider)?,
290 AppType::OpenCode => {}
291 AppType::OpenClaw => {}
292 AppType::Hermes => {}
293 }
294
295 Ok(())
296 }
297
298 fn sync_codex_live(
299 config: &mut MultiAppConfig,
300 provider_id: &str,
301 provider: &Provider,
302 ) -> Result<(), AppError> {
303 let common_config_snippet = config.common_config_snippets.codex.clone();
304 let effective = ProviderService::build_effective_live_snapshot(
305 &AppType::Codex,
306 provider,
307 common_config_snippet.as_deref(),
308 true,
309 )?;
310 let settings = effective.as_object().ok_or_else(|| {
311 AppError::Config(format!("供应商 {provider_id} 的 Codex 配置必须是对象"))
312 })?;
313 let auth = settings.get("auth").ok_or_else(|| {
314 AppError::Config(format!("供应商 {provider_id} 的 Codex 配置缺少 auth 字段"))
315 })?;
316 if !auth.is_object() {
317 return Err(AppError::Config(format!(
318 "供应商 {provider_id} 的 Codex auth 配置必须是 JSON 对象"
319 )));
320 }
321 let cfg_text = settings.get("config").and_then(Value::as_str);
322
323 crate::codex_config::write_codex_live_atomic_with_stable_provider(auth, cfg_text)?;
324 crate::mcp::sync_enabled_to_codex(config)?;
325
326 let cfg_text_after = crate::codex_config::read_and_validate_codex_config_text()?;
327 if let Some(manager) = config.get_manager_mut(&AppType::Codex) {
328 if let Some(target) = manager.providers.get_mut(provider_id) {
329 let mut raw_settings = serde_json::Map::new();
330 raw_settings.insert("auth".to_string(), auth.clone());
331 raw_settings.insert(
332 "config".to_string(),
333 serde_json::Value::String(cfg_text_after),
334 );
335 target.settings_config = ProviderService::normalize_settings_config_for_storage(
336 &AppType::Codex,
337 provider,
338 serde_json::Value::Object(raw_settings),
339 common_config_snippet.as_deref(),
340 )?;
341 }
342 }
343
344 Ok(())
345 }
346
347 fn sync_claude_live(
348 config: &mut MultiAppConfig,
349 provider_id: &str,
350 provider: &Provider,
351 ) -> Result<(), AppError> {
352 use crate::config::{read_json_file, write_json_file};
353
354 let settings_path = crate::config::get_claude_settings_path();
355 if let Some(parent) = settings_path.parent() {
356 fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?;
357 }
358
359 let common_config_snippet = config.common_config_snippets.claude.clone();
360 let effective = ProviderService::build_effective_live_snapshot(
361 &AppType::Claude,
362 provider,
363 common_config_snippet.as_deref(),
364 true,
365 )?;
366
367 write_json_file(&settings_path, &effective)?;
368
369 let live_after = read_json_file::<serde_json::Value>(&settings_path)?;
370 if let Some(manager) = config.get_manager_mut(&AppType::Claude) {
371 if let Some(target) = manager.providers.get_mut(provider_id) {
372 target.settings_config = ProviderService::normalize_settings_config_for_storage(
373 &AppType::Claude,
374 provider,
375 live_after,
376 common_config_snippet.as_deref(),
377 )?;
378 }
379 }
380
381 Ok(())
382 }
383
384 fn sync_gemini_live(
385 config: &mut MultiAppConfig,
386 provider_id: &str,
387 provider: &Provider,
388 ) -> Result<(), AppError> {
389 use crate::gemini_config::{env_to_json, read_gemini_env};
390
391 let common_config_snippet = config.common_config_snippets.gemini.clone();
392 ProviderService::write_gemini_live_force(provider, common_config_snippet.as_deref())?;
393
394 let live_after_env = read_gemini_env()?;
396 let settings_path = crate::gemini_config::get_gemini_settings_path();
397 let live_after_config = if settings_path.exists() {
398 crate::config::read_json_file(&settings_path)?
399 } else {
400 serde_json::json!({})
401 };
402 let mut live_after = env_to_json(&live_after_env);
403 if let Some(obj) = live_after.as_object_mut() {
404 obj.insert("config".to_string(), live_after_config);
405 }
406
407 if let Some(manager) = config.get_manager_mut(&AppType::Gemini) {
408 if let Some(target) = manager.providers.get_mut(provider_id) {
409 target.settings_config = ProviderService::normalize_settings_config_for_storage(
410 &AppType::Gemini,
411 provider,
412 live_after,
413 common_config_snippet.as_deref(),
414 )?;
415 }
416 }
417
418 Ok(())
419 }
420}