Files
clash-verge-rev/src-tauri/src/cmd/save_profile.rs
T
Tunglies b85b435e00 refactor: structure config validation outcomes
- Replace boolean/string validation results with structured ValidationOutcome values across config validation, profile switching, DNS validation, and script validation. Callers can now distinguish valid, invalid, skipped, and busy states, with invalid results carrying an error kind and message.

- Route core config updates through structured outcomes and checked forced updates. Generation and validation failures now discard runtime drafts and surface validation failures instead of relying on empty success messages.

- Preserve existing frontend ergonomics by keeping boolean service wrappers where callers only need success, while direct DNS and script validation consumers receive ValidationOutcome. Failed profile editor saves reload the backend-restored file content and synchronize Monaco models so reopened editors show the restored state.
2026-04-25 13:31:08 +08:00

179 lines
5.8 KiB
Rust

use super::CmdResult;
use crate::{
cmd::StringifyErr as _,
cmd::validate::{ValidationNoticeTarget, handle_validation_notice},
config::{Config, IProfiles, PrfItem},
core::{
CoreManager, handle,
validate::{CoreConfigValidator, ValidationOutcome},
},
module::auto_backup::{AutoBackupManager, AutoBackupTrigger},
utils::dirs,
};
use clash_verge_logging::{Type, logging};
use smartstring::alias::String;
use tokio::fs;
/// 保存profiles的配置
#[tauri::command]
pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult<ValidationOutcome> {
let file_data = match file_data {
Some(d) => d,
None => return Ok(ValidationOutcome::Valid),
};
let backup_trigger = match index.as_str() {
"Merge" => Some(AutoBackupTrigger::GlobalMerge),
"Script" => Some(AutoBackupTrigger::GlobalScript),
_ => None,
};
// 在异步操作前获取必要元数据并释放锁
let (rel_path, is_merge_file, is_script_file, affects_runtime) = {
let profiles = Config::profiles().await;
let profiles_guard = profiles.latest_arc();
let item = profiles_guard.get_item(&index).stringify_err()?;
let is_merge = item.itype.as_ref().is_some_and(|t| t == "merge");
let path = item.file.clone().ok_or("file field is null")?;
let is_script = item.itype.as_ref().is_some_and(|t| t == "script") || path.ends_with(".js");
let affects_runtime = profile_affects_runtime(&profiles_guard, &index);
(path, is_merge, is_script, affects_runtime)
};
// 读取原始内容(在释放profiles_guard后进行)
let original_content = PrfItem {
file: Some(rel_path.clone()),
..Default::default()
}
.read_file()
.await
.stringify_err()?;
let profiles_dir = dirs::app_profiles_dir().stringify_err()?;
let file_path = profiles_dir.join(rel_path.as_str());
let file_path_str = file_path.to_string_lossy().to_string();
// 保存新的配置文件
fs::write(&file_path, &file_data).await.stringify_err()?;
logging!(
info,
Type::Config,
"[cmd配置save] 开始验证配置文件: {}, 是否为merge文件: {}",
file_path_str,
is_merge_file
);
let changes_applied = handle_saved_profile_file(
&file_path_str,
&file_path,
&original_content,
is_merge_file,
is_script_file,
affects_runtime,
)
.await?;
if changes_applied.is_valid()
&& let Some(trigger) = backup_trigger
{
AutoBackupManager::trigger_backup(trigger);
}
Ok(changes_applied)
}
async fn restore_original(file_path: &std::path::Path, original_content: &str) -> Result<(), String> {
fs::write(file_path, original_content).await.stringify_err()
}
fn profile_affects_runtime(profiles: &IProfiles, index: &str) -> bool {
let Some(current_uid) = profiles.get_current() else {
return false;
};
if current_uid == index {
return true;
}
let Ok(item) = profiles.get_item(current_uid) else {
return false;
};
[
item.current_merge().map_or("Merge", String::as_str),
item.current_script().map_or("Script", String::as_str),
item.current_rules().map_or("Rules", String::as_str),
item.current_proxies().map_or("Proxies", String::as_str),
item.current_groups().map_or("Groups", String::as_str),
]
.contains(&index)
}
async fn handle_saved_profile_file(
file_path_str: &str,
file_path: &std::path::Path,
original_content: &str,
is_merge_file: bool,
is_script_file: bool,
affects_runtime: bool,
) -> CmdResult<ValidationOutcome> {
let (target, file_type) = if is_script_file {
(ValidationNoticeTarget::Script, "脚本文件")
} else if is_merge_file {
(ValidationNoticeTarget::Merge, "合并配置文件")
} else {
(ValidationNoticeTarget::Runtime, "YAML配置文件")
};
logging!(
info,
Type::Config,
"[cmd配置save] 开始{}验证: {}",
file_type,
file_path_str
);
match CoreConfigValidator::validate_config_file_outcome(file_path_str, Some(is_merge_file)).await {
Ok(outcome) if outcome.is_valid() => {
logging!(info, Type::Config, "[cmd配置save] 文件验证通过: {}", file_path_str);
}
Ok(outcome) => {
logging!(warn, Type::Config, "[cmd配置save] 文件验证失败: {}", outcome);
restore_original(file_path, original_content).await?;
handle_validation_notice(&outcome, target, file_type);
return Ok(outcome);
}
Err(e) => {
logging!(error, Type::Config, "[cmd配置save] 验证过程发生错误: {}", e);
restore_original(file_path, original_content).await?;
return Err(e.to_string().into());
}
}
if !affects_runtime {
return Ok(ValidationOutcome::Valid);
}
logging!(
info,
Type::Config,
"[cmd配置save] 保存项影响当前运行时配置,开始统一应用"
);
match CoreManager::global().update_config_forced().await {
Ok(outcome) if outcome.is_valid() => {
handle::Handle::refresh_clash();
Ok(ValidationOutcome::Valid)
}
Ok(outcome) => {
logging!(warn, Type::Config, "[cmd配置save] 运行时配置应用失败: {}", outcome);
restore_original(file_path, original_content).await?;
handle_validation_notice(&outcome, ValidationNoticeTarget::Runtime, "运行时配置");
Ok(outcome)
}
Err(err) => {
logging!(error, Type::Config, "[cmd配置save] 运行时配置应用错误: {}", err);
restore_original(file_path, original_content).await?;
Err(err.to_string().into())
}
}
}