mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-05-28 01:09:23 +08:00
fix(backup): restrict SQL import to CC Switch exported backups only
- Add validation to reject SQL files without CC Switch export header - Remove redundant sanitize_import_sql (sqlite_* objects already excluded at export time) - Fix backup filename collision by appending counter suffix - Update i18n hints to clarify import restriction
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -1003,3 +1003,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"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -150,7 +150,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.",
|
||||
|
||||
@@ -150,7 +150,7 @@
|
||||
"themeDark": "ダーク",
|
||||
"themeSystem": "システム",
|
||||
"importExport": "SQL インポート/エクスポート",
|
||||
"importExportHint": "移行や復元用にデータベースの SQL バックアップをインポート/エクスポートします。",
|
||||
"importExportHint": "移行や復元用にデータベースの SQL バックアップをインポート/エクスポートします(インポートは CC Switch がエクスポートしたバックアップのみ対応)。",
|
||||
"exportConfig": "SQL バックアップをエクスポート",
|
||||
"selectConfigFile": "SQL ファイルを選択",
|
||||
"noFileSelected": "ファイルが選択されていません。",
|
||||
|
||||
@@ -150,7 +150,7 @@
|
||||
"themeDark": "深色",
|
||||
"themeSystem": "跟随系统",
|
||||
"importExport": "SQL 导入导出",
|
||||
"importExportHint": "导入/导出数据库 SQL 备份,便于备份或迁移。",
|
||||
"importExportHint": "导入/导出数据库 SQL 备份(仅支持导入由 CC Switch 导出的备份),便于备份或迁移。",
|
||||
"exportConfig": "导出 SQL 备份",
|
||||
"selectConfigFile": "选择 SQL 文件",
|
||||
"noFileSelected": "尚未选择配置文件。",
|
||||
|
||||
Reference in New Issue
Block a user