mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-23 20:22:12 +08:00
* refactor(ipc): generate bindings with Specta * fix(profile): project IPC inputs to consumed fields * fix(events): remove unused refresh payloads * fix(webdav): return only consumed backup metadata * fix(network): trim unused interface metadata * fix(sysinfo): return only displayed system fields * chore(knip): check generated runtime exports * fix(proxy): confirm runtime chain updates before UI changes * fix(clipboard): propagate proxy environment copy failures * refactor(dns): remove ignored runtime patch * refactor(core): trim unused upgrade response fields * refactor(ipc): avoid redundant response copies * refactor(proxy): clarify automatic proxy query naming * chore(tests): remove added IPC regression tests * refactor(ipc): enforce typed command boundaries * chore(contract): check generated bindings before push * refactor(ipc): remove unused proxy liveness payloads
188 lines
6.2 KiB
Rust
188 lines
6.2 KiB
Rust
use super::CmdResult;
|
|
use crate::{
|
|
cmd::StringifyErr as _,
|
|
cmd::validate::{ValidationNoticeTarget, handle_validation_notice},
|
|
config::{Config, IProfiles, PrfItem},
|
|
core::auto_backup::{AutoBackupManager, AutoBackupTrigger},
|
|
core::{
|
|
CoreManager, handle,
|
|
validate::{CoreConfigValidator, ValidationOutcome},
|
|
},
|
|
utils::dirs,
|
|
};
|
|
use clash_verge_logging::{Type, logging, logging_error};
|
|
use smartstring::alias::String;
|
|
use tokio::fs;
|
|
|
|
/// 保存profiles的配置
|
|
#[tauri::command]
|
|
#[specta::specta]
|
|
pub async fn save_profile_file(
|
|
index: std::string::String,
|
|
file_data: Option<std::string::String>,
|
|
) -> CmdResult<ValidationOutcome> {
|
|
let index: String = index.into();
|
|
let file_data = file_data.map(String::from);
|
|
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)
|
|
};
|
|
|
|
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();
|
|
|
|
// 读取原始内容(在释放profiles_guard后进行)
|
|
let original_existed = fs::try_exists(&file_path).await.map_err(|err| {
|
|
String::from(format!(
|
|
"failed to check profile file \"{}\": {err}",
|
|
file_path.display()
|
|
))
|
|
})?;
|
|
let original_content = if original_existed {
|
|
PrfItem {
|
|
file: Some(rel_path.clone()),
|
|
..Default::default()
|
|
}
|
|
.read_file()
|
|
.await
|
|
.stringify_err()?
|
|
} else {
|
|
String::new()
|
|
};
|
|
|
|
// 保存新的配置文件
|
|
fs::write(&file_path, &file_data).await.stringify_err()?;
|
|
|
|
let changes_applied = handle_saved_profile_file(
|
|
&file_path_str,
|
|
&file_path,
|
|
&original_content,
|
|
original_existed,
|
|
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,
|
|
original_existed: bool,
|
|
) -> CmdResult<()> {
|
|
if original_existed {
|
|
fs::write(file_path, original_content).await.stringify_err()
|
|
} else {
|
|
fs::remove_file(file_path).await.stringify_err()
|
|
}
|
|
}
|
|
|
|
fn profile_affects_runtime(profiles: &IProfiles, index: &str) -> bool {
|
|
let Some(current_uid) = profiles.current.as_ref() 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,
|
|
original_existed: bool,
|
|
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配置文件")
|
|
};
|
|
|
|
match CoreConfigValidator::validate_config_file_outcome(file_path_str, Some(is_merge_file)).await {
|
|
Ok(outcome) if outcome.is_valid() => {}
|
|
Ok(outcome) => {
|
|
logging!(warn, Type::Config, "[cmd配置save] 文件验证失败: {}", outcome);
|
|
restore_original(file_path, original_content, original_existed).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, original_existed).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() => {
|
|
logging_error!(Type::Config, Config::sync_dns_override().await);
|
|
handle::Handle::refresh_clash();
|
|
Ok(ValidationOutcome::Valid)
|
|
}
|
|
Ok(outcome) => {
|
|
logging!(warn, Type::Config, "[cmd配置save] 运行时配置应用失败: {}", outcome);
|
|
restore_original(file_path, original_content, original_existed).await?;
|
|
handle_validation_notice(&outcome, ValidationNoticeTarget::Runtime, "运行时配置");
|
|
Ok(outcome)
|
|
}
|
|
Err(err) => {
|
|
logging!(error, Type::Config, "[cmd配置save] 运行时配置应用错误: {err:#}");
|
|
restore_original(file_path, original_content, original_existed).await?;
|
|
Err(err.to_string().into())
|
|
}
|
|
}
|
|
}
|