From 79370dd8a1fb7d9df110fe0795c5c865b8cbbfbb Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 4 Sep 2025 16:07:38 +0800 Subject: [PATCH] feat(backup): archive current live config before switching\n\n- Archive Claude settings.json and Codex auth.json/config.toml to ~/.cc-switch/archive/\n- Preserve user edits while centralizing SSOT in cc-switch config.json\n- Uses atomic writes for all subsequent updates --- src-tauri/src/commands.rs | 15 ++++++++++++ src-tauri/src/config.rs | 49 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 954fdfb..ec25458 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -258,6 +258,14 @@ pub async fn switch_provider( .map_err(|e| format!("创建 Codex 目录失败: {}", e))?; } + // 备份当前 live 文件到归档(单一数据源:以 cc-switch 配置为主,但保护用户手改历史) + let ts = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + let _ = crate::config::archive_file(ts, "codex", &auth_path); + let _ = crate::config::archive_file(ts, "codex", &config_path); + // 写 auth.json(必需) let auth = provider .settings_config @@ -303,6 +311,13 @@ pub async fn switch_provider( if let Some(parent) = settings_path.parent() { std::fs::create_dir_all(parent).map_err(|e| format!("创建目录失败: {}", e))?; } + + // 备份当前 live 文件到归档 + let ts = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + let _ = crate::config::archive_file(ts, "claude", &settings_path); write_json_file(&settings_path, &provider.settings_config)?; } } diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 762ab52..2b58268 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -39,6 +39,55 @@ pub fn get_app_config_path() -> PathBuf { get_app_config_dir().join("config.json") } +/// 归档根目录 ~/.cc-switch/archive +pub fn get_archive_root() -> PathBuf { + get_app_config_dir().join("archive") +} + +fn ensure_unique_path(mut dest: PathBuf) -> PathBuf { + if !dest.exists() { + return dest; + } + let file_name = dest + .file_stem() + .map(|s| s.to_string_lossy().to_string()) + .unwrap_or_else(|| "file".into()); + let ext = dest + .extension() + .map(|s| format!(".{}", s.to_string_lossy())) + .unwrap_or_default(); + let parent = dest.parent().map(|p| p.to_path_buf()).unwrap_or_default(); + for i in 2..1000 { + let mut candidate = parent.clone(); + candidate.push(format!("{}-{}{}", file_name, i, ext)); + if !candidate.exists() { + return candidate; + } + } + dest +} + +/// 将现有文件归档到 `~/.cc-switch/archive///` 下,返回归档路径 +pub fn archive_file(ts: u64, category: &str, src: &Path) -> Result, String> { + if !src.exists() { + return Ok(None); + } + let mut dest_dir = get_archive_root(); + dest_dir.push(ts.to_string()); + dest_dir.push(category); + fs::create_dir_all(&dest_dir).map_err(|e| format!("创建归档目录失败: {}", e))?; + + let file_name = src + .file_name() + .map(|s| s.to_string_lossy().to_string()) + .unwrap_or_else(|| "file".into()); + let mut dest = dest_dir.join(file_name); + dest = ensure_unique_path(dest); + + copy_file(src, &dest)?; + Ok(Some(dest)) +} + /// 清理供应商名称,确保文件名安全 pub fn sanitize_provider_name(name: &str) -> String { name.chars()