use super::provider::ProviderService;
use crate::app_config::{AppType, MultiAppConfig};
use crate::database::Database;
use crate::error::AppError;
use crate::provider::Provider;
use crate::store::AppState;
use chrono::Utc;
use serde_json::Value;
use std::fs;
use std::path::{Path, PathBuf};
const MAX_BACKUPS: usize = 10;
#[derive(Debug, Clone)]
pub struct BackupInfo {
pub id: String,
pub path: PathBuf,
pub timestamp: String,
pub display_name: String,
}
pub struct ConfigService;
impl ConfigService {
pub fn create_backup(
config_path: &Path,
custom_name: Option<String>,
) -> Result<String, AppError> {
let db_path = crate::config::get_app_config_dir().join("cc-switch.db");
if !db_path.exists() {
return Ok(String::new());
}
let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
let backup_id = if let Some(name) = custom_name {
format!("{}_{}", name, timestamp)
} else {
format!("backup_{}", timestamp)
};
let backup_dir = config_path
.parent()
.or_else(|| db_path.parent())
.ok_or_else(|| AppError::Config("Invalid config path".into()))?
.join("backups");
fs::create_dir_all(&backup_dir).map_err(|e| AppError::io(&backup_dir, e))?;
let backup_path = backup_dir.join(format!("{backup_id}.sql"));
let db = Database::init()?;
db.export_sql(&backup_path)?;
Self::cleanup_old_backups(&backup_dir, MAX_BACKUPS)?;
Ok(backup_id)
}
pub fn list_backups(config_path: &Path) -> Result<Vec<BackupInfo>, AppError> {
let backup_dir = config_path
.parent()
.ok_or_else(|| AppError::Config("Invalid config path".into()))?
.join("backups");
if !backup_dir.exists() {
return Ok(Vec::new());
}
let entries = fs::read_dir(&backup_dir).map_err(|e| AppError::io(&backup_dir, e))?;
let mut backups: Vec<BackupInfo> = entries
.filter_map(|entry| entry.ok())
.filter(|entry| {
entry
.path()
.extension()
.map(|ext| ext == "sql")
.unwrap_or(false)
})
.filter_map(|entry| {
let path = entry.path();
let filename = path.file_stem()?.to_str()?.to_string();
let timestamp = Self::extract_timestamp(&filename)?;
let display_name = Self::format_display_name(&filename, ×tamp);
Some(BackupInfo {
id: filename.clone(),
path: path.clone(),
timestamp,
display_name,
})
})
.collect();
backups.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
Ok(backups)
}
pub fn restore_from_backup_id(backup_id: &str, state: &AppState) -> Result<String, AppError> {
let config_path = crate::config::get_app_config_path();
let backup_dir = config_path
.parent()
.ok_or_else(|| AppError::Config("Invalid config path".into()))?
.join("backups");
let backup_path = backup_dir.join(format!("{}.sql", backup_id));
if !backup_path.exists() {
return Err(AppError::Message(format!("备份文件不存在: {}", backup_id)));
}
Self::import_config_from_path(&backup_path, state)
}
fn extract_timestamp(filename: &str) -> Option<String> {
let parts: Vec<&str> = filename.rsplitn(3, '_').collect();
if parts.len() >= 2 {
Some(format!("{}_{}", parts[1], parts[0]))
} else {
None
}
}
fn format_display_name(filename: &str, timestamp: &str) -> String {
if timestamp.len() == 15 {
let date = ×tamp[0..8];
let time = ×tamp[9..15];
if let (Ok(y), Ok(m), Ok(d), Ok(h), Ok(min), Ok(s)) = (
date[0..4].parse::<u32>(),
date[4..6].parse::<u32>(),
date[6..8].parse::<u32>(),
time[0..2].parse::<u32>(),
time[2..4].parse::<u32>(),
time[4..6].parse::<u32>(),
) {
let formatted_time =
format!("{:04}-{:02}-{:02} {:02}:{:02}:{:02}", y, m, d, h, min, s);
if !filename.starts_with("backup_") {
let custom_name = filename.rsplitn(3, '_').nth(2).unwrap_or(filename);
return format!("{} ({})", custom_name, formatted_time);
}
return formatted_time;
}
}
filename.to_string()
}
fn cleanup_old_backups(backup_dir: &Path, retain: usize) -> Result<(), AppError> {
if retain == 0 {
return Ok(());
}
let entries = match fs::read_dir(backup_dir) {
Ok(iter) => iter
.filter_map(|entry| entry.ok())
.filter(|entry| {
entry
.path()
.extension()
.map(|ext| ext == "sql")
.unwrap_or(false)
})
.collect::<Vec<_>>(),
Err(_) => return Ok(()),
};
if entries.len() <= retain {
return Ok(());
}
let remove_count = entries.len().saturating_sub(retain);
let mut sorted = entries;
sorted.sort_by(|a, b| {
let a_time = a.metadata().and_then(|m| m.modified()).ok();
let b_time = b.metadata().and_then(|m| m.modified()).ok();
a_time.cmp(&b_time)
});
for entry in sorted.into_iter().take(remove_count) {
if let Err(err) = fs::remove_file(entry.path()) {
log::warn!(
"Failed to remove old backup {}: {}",
entry.path().display(),
err
);
}
}
Ok(())
}
pub fn export_config_to_path(target_path: &Path) -> Result<(), AppError> {
let db = Database::init()?;
db.export_sql(target_path)
}
pub fn import_config_from_path(file_path: &Path, state: &AppState) -> Result<String, AppError> {
let db_path = crate::config::get_app_config_dir().join("cc-switch.db");
if !db_path.exists() {
return Err(AppError::Config("数据库不存在,无法导入".to_string()));
}
let backup_id = Self::create_backup(&db_path, None)?;
state.db.import_sql(file_path)?;
state.refresh_config_from_db()?;
Ok(backup_id)
}
pub fn sync_current_providers_to_live(config: &mut MultiAppConfig) -> Result<(), AppError> {
Self::sync_current_provider_for_app(config, &AppType::Claude)?;
Self::sync_current_provider_for_app(config, &AppType::Codex)?;
Self::sync_current_provider_for_app(config, &AppType::Gemini)?;
Self::sync_current_provider_for_app(config, &AppType::OpenCode)?;
Self::sync_current_provider_for_app(config, &AppType::OpenClaw)?;
Ok(())
}
fn sync_current_provider_for_app(
config: &mut MultiAppConfig,
app_type: &AppType,
) -> Result<(), AppError> {
let (current_id, provider) = {
let manager = match config.get_manager(app_type) {
Some(manager) => manager,
None => return Ok(()),
};
if manager.current.is_empty() {
return Ok(());
}
let current_id = manager.current.clone();
let provider = match manager.providers.get(¤t_id) {
Some(provider) => provider.clone(),
None => {
log::warn!(
"当前应用 {app_type:?} 的供应商 {current_id} 不存在,跳过 live 同步"
);
return Ok(());
}
};
(current_id, provider)
};
match app_type {
AppType::Codex => Self::sync_codex_live(config, ¤t_id, &provider)?,
AppType::Claude => Self::sync_claude_live(config, ¤t_id, &provider)?,
AppType::Gemini => Self::sync_gemini_live(config, ¤t_id, &provider)?,
AppType::OpenCode => {}
AppType::OpenClaw => {}
AppType::Hermes => {}
}
Ok(())
}
fn sync_codex_live(
config: &mut MultiAppConfig,
provider_id: &str,
provider: &Provider,
) -> Result<(), AppError> {
let common_config_snippet = config.common_config_snippets.codex.clone();
let effective = ProviderService::build_effective_live_snapshot(
&AppType::Codex,
provider,
common_config_snippet.as_deref(),
true,
)?;
let settings = effective.as_object().ok_or_else(|| {
AppError::Config(format!("供应商 {provider_id} 的 Codex 配置必须是对象"))
})?;
let auth = settings.get("auth").ok_or_else(|| {
AppError::Config(format!("供应商 {provider_id} 的 Codex 配置缺少 auth 字段"))
})?;
if !auth.is_object() {
return Err(AppError::Config(format!(
"供应商 {provider_id} 的 Codex auth 配置必须是 JSON 对象"
)));
}
let cfg_text = settings.get("config").and_then(Value::as_str);
crate::codex_config::write_codex_live_atomic_with_stable_provider(auth, cfg_text)?;
crate::mcp::sync_enabled_to_codex(config)?;
let cfg_text_after = crate::codex_config::read_and_validate_codex_config_text()?;
if let Some(manager) = config.get_manager_mut(&AppType::Codex) {
if let Some(target) = manager.providers.get_mut(provider_id) {
let mut raw_settings = serde_json::Map::new();
raw_settings.insert("auth".to_string(), auth.clone());
raw_settings.insert(
"config".to_string(),
serde_json::Value::String(cfg_text_after),
);
target.settings_config = ProviderService::normalize_settings_config_for_storage(
&AppType::Codex,
provider,
serde_json::Value::Object(raw_settings),
common_config_snippet.as_deref(),
)?;
}
}
Ok(())
}
fn sync_claude_live(
config: &mut MultiAppConfig,
provider_id: &str,
provider: &Provider,
) -> Result<(), AppError> {
use crate::config::{read_json_file, write_json_file};
let settings_path = crate::config::get_claude_settings_path();
if let Some(parent) = settings_path.parent() {
fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?;
}
let common_config_snippet = config.common_config_snippets.claude.clone();
let effective = ProviderService::build_effective_live_snapshot(
&AppType::Claude,
provider,
common_config_snippet.as_deref(),
true,
)?;
write_json_file(&settings_path, &effective)?;
let live_after = read_json_file::<serde_json::Value>(&settings_path)?;
if let Some(manager) = config.get_manager_mut(&AppType::Claude) {
if let Some(target) = manager.providers.get_mut(provider_id) {
target.settings_config = ProviderService::normalize_settings_config_for_storage(
&AppType::Claude,
provider,
live_after,
common_config_snippet.as_deref(),
)?;
}
}
Ok(())
}
fn sync_gemini_live(
config: &mut MultiAppConfig,
provider_id: &str,
provider: &Provider,
) -> Result<(), AppError> {
use crate::gemini_config::{env_to_json, read_gemini_env};
let common_config_snippet = config.common_config_snippets.gemini.clone();
ProviderService::write_gemini_live_force(provider, common_config_snippet.as_deref())?;
let live_after_env = read_gemini_env()?;
let settings_path = crate::gemini_config::get_gemini_settings_path();
let live_after_config = if settings_path.exists() {
crate::config::read_json_file(&settings_path)?
} else {
serde_json::json!({})
};
let mut live_after = env_to_json(&live_after_env);
if let Some(obj) = live_after.as_object_mut() {
obj.insert("config".to_string(), live_after_config);
}
if let Some(manager) = config.get_manager_mut(&AppType::Gemini) {
if let Some(target) = manager.providers.get_mut(provider_id) {
target.settings_config = ProviderService::normalize_settings_config_for_storage(
&AppType::Gemini,
provider,
live_after,
common_config_snippet.as_deref(),
)?;
}
}
Ok(())
}
}