diff --git a/src-tauri/src/database/backup.rs b/src-tauri/src/database/backup.rs index 631bc30e7..bd3df4181 100644 --- a/src-tauri/src/database/backup.rs +++ b/src-tauri/src/database/backup.rs @@ -13,6 +13,8 @@ use std::fs; use std::path::{Path, PathBuf}; use tempfile::NamedTempFile; +const CC_SWITCH_SQL_EXPORT_HEADER: &str = "-- CC Switch SQLite 导出"; + impl Database { /// 导出为 SQLite 兼容的 SQL 文本 pub fn export_sql(&self, target_path: &Path) -> Result<(), AppError> { @@ -36,7 +38,8 @@ impl Database { } let sql_raw = fs::read_to_string(source_path).map_err(|e| AppError::io(source_path, e))?; - let sql_content = Self::sanitize_import_sql(&sql_raw); + let sql_content = sql_raw.trim_start_matches('\u{feff}'); + Self::validate_cc_switch_sql_export(sql_content)?; // 导入前备份现有数据库 let backup_path = self.backup_database_file()?; @@ -51,7 +54,7 @@ impl Database { Connection::open(&temp_path).map_err(|e| AppError::Database(e.to_string()))?; temp_conn - .execute_batch(&sql_content) + .execute_batch(sql_content) .map_err(|e| AppError::Database(format!("执行 SQL 导入失败: {e}")))?; // 补齐缺失表/索引并进行基础校验 @@ -93,26 +96,17 @@ impl Database { Ok(snapshot) } - /// 移除 SQLite 保留对象相关语句(如 sqlite_sequence),避免导入报错 - fn sanitize_import_sql(sql: &str) -> String { - let mut cleaned = String::new(); - let lower_keyword = "sqlite_sequence"; - - for stmt in sql.split(';') { - let trimmed = stmt.trim(); - if trimmed.is_empty() { - continue; - } - - if trimmed.to_ascii_lowercase().contains(lower_keyword) { - continue; - } - - cleaned.push_str(trimmed); - cleaned.push_str(";\n"); + fn validate_cc_switch_sql_export(sql: &str) -> Result<(), AppError> { + let trimmed = sql.trim_start(); + if trimmed.starts_with(CC_SWITCH_SQL_EXPORT_HEADER) { + return Ok(()); } - cleaned + Err(AppError::localized( + "backup.sql.invalid_format", + "仅支持导入由 CC Switch 导出的 SQL 备份文件。", + "Only SQL backups exported by CC Switch are supported.", + )) } /// 生成一致性快照备份,返回备份文件路径(不存在主库时返回 None) @@ -129,8 +123,15 @@ impl Database { fs::create_dir_all(&backup_dir).map_err(|e| AppError::io(&backup_dir, e))?; - let backup_id = format!("db_backup_{}", Utc::now().format("%Y%m%d_%H%M%S")); - let backup_path = backup_dir.join(format!("{backup_id}.db")); + let base_id = format!("db_backup_{}", Utc::now().format("%Y%m%d_%H%M%S")); + let mut backup_id = base_id.clone(); + let mut backup_path = backup_dir.join(format!("{backup_id}.db")); + let mut counter = 1; + while backup_path.exists() { + backup_id = format!("{base_id}_{counter}"); + backup_path = backup_dir.join(format!("{backup_id}.db")); + counter += 1; + } { let conn = lock_conn!(self.conn); diff --git a/src-tauri/tests/import_export_sync.rs b/src-tauri/tests/import_export_sync.rs index 9a853aebb..81e01c8d8 100644 --- a/src-tauri/tests/import_export_sync.rs +++ b/src-tauri/tests/import_export_sync.rs @@ -993,3 +993,76 @@ fn export_sql_returns_error_for_invalid_path() { other => panic!("expected IoContext or Io error, got {other:?}"), } } + +#[test] +fn import_sql_rejects_non_cc_switch_backup() { + let _guard = test_mutex().lock().expect("acquire test mutex"); + reset_test_fs(); + let home = ensure_test_home(); + + let state = create_test_state().expect("create test state"); + + let import_path = home.join("not-cc-switch.sql"); + fs::write(&import_path, "CREATE TABLE x (id INTEGER);").expect("write import sql"); + + let err = state + .db + .import_sql(&import_path) + .expect_err("non-cc-switch sql should be rejected"); + + match err { + AppError::Localized { key, .. } => { + assert_eq!(key, "backup.sql.invalid_format"); + } + other => panic!("expected Localized error, got {other:?}"), + } +} + +#[test] +fn import_sql_accepts_cc_switch_exported_backup() { + let _guard = test_mutex().lock().expect("acquire test mutex"); + reset_test_fs(); + let home = ensure_test_home(); + + // Create a database with some data and export it. + let mut config = MultiAppConfig::default(); + { + let manager = config + .get_manager_mut(&AppType::Claude) + .expect("claude manager"); + manager.current = "test-provider".to_string(); + manager.providers.insert( + "test-provider".to_string(), + Provider::with_id( + "test-provider".to_string(), + "Test Provider".to_string(), + json!({"env": {"ANTHROPIC_API_KEY": "test-key"}}), + None, + ), + ); + } + + let state = create_test_state_with_config(&config).expect("create test state"); + let export_path = home.join("cc-switch-export.sql"); + state + .db + .export_sql(&export_path) + .expect("export should succeed"); + + // Reset database, then import into a fresh one. + reset_test_fs(); + let state = create_test_state().expect("create test state"); + state + .db + .import_sql(&export_path) + .expect("import should succeed"); + + let providers = state + .db + .get_all_providers(AppType::Claude.as_str()) + .expect("load providers"); + assert!( + providers.contains_key("test-provider"), + "imported providers should contain test-provider" + ); +} diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 92820c5ab..3f08519b0 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -146,7 +146,7 @@ "themeDark": "Dark", "themeSystem": "System", "importExport": "SQL Import/Export", - "importExportHint": "Import or export database SQL backups for migration or restore.", + "importExportHint": "Import or export database SQL backups for migration or restore (import supports only backups exported by CC Switch).", "exportConfig": "Export SQL Backup", "selectConfigFile": "Select SQL File", "noFileSelected": "No configuration file selected.", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 0fab2caa1..c4b25c649 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -146,7 +146,7 @@ "themeDark": "ダーク", "themeSystem": "システム", "importExport": "SQL インポート/エクスポート", - "importExportHint": "移行や復元用にデータベースの SQL バックアップをインポート/エクスポートします。", + "importExportHint": "移行や復元用にデータベースの SQL バックアップをインポート/エクスポートします(インポートは CC Switch がエクスポートしたバックアップのみ対応)。", "exportConfig": "SQL バックアップをエクスポート", "selectConfigFile": "SQL ファイルを選択", "noFileSelected": "ファイルが選択されていません。", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 6eacfbfde..d0955b5a6 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -146,7 +146,7 @@ "themeDark": "深色", "themeSystem": "跟随系统", "importExport": "SQL 导入导出", - "importExportHint": "导入/导出数据库 SQL 备份,便于备份或迁移。", + "importExportHint": "导入/导出数据库 SQL 备份(仅支持导入由 CC Switch 导出的备份),便于备份或迁移。", "exportConfig": "导出 SQL 备份", "selectConfigFile": "选择 SQL 文件", "noFileSelected": "尚未选择配置文件。",