diff --git a/src-tauri/src/config/config.rs b/src-tauri/src/config/config.rs index 3fa1ec3c9..f086f8694 100644 --- a/src-tauri/src/config/config.rs +++ b/src-tauri/src/config/config.rs @@ -132,20 +132,11 @@ impl Config { Self::runtime().await.apply(); - { - let profiles = Self::profiles().await.data_arc(); - let _ = profiles.cleanup_orphaned_files().await; - } - Ok(()) } async fn ensure_default_profile_items() -> Result<()> { let profiles = Self::profiles().await; - Self::ensure_default_profile_items_for(&profiles).await - } - - async fn ensure_default_profile_items_for(profiles: &Draft) -> Result<()> { if profiles.latest_arc().items.is_none() { logging!( warn, @@ -157,11 +148,11 @@ impl Config { if profiles.latest_arc().get_item("Merge").is_err() { let merge_item = &mut PrfItem::from_merge(Some("Merge".into())); - profiles_append_item_to_safe(profiles, merge_item).await?; + profiles_append_item_to_safe(&profiles, merge_item).await?; } if profiles.latest_arc().get_item("Script").is_err() { let script_item = &mut PrfItem::from_script(Some("Script".into())); - profiles_append_item_to_safe(profiles, script_item).await?; + profiles_append_item_to_safe(&profiles, script_item).await?; } Ok(()) } @@ -380,27 +371,3 @@ pub(crate) enum ConfigType { Run, Check, } - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn failed_profile_index_survives_startup_without_cleanup() -> Result<()> { - let profiles = Draft::new(IProfiles::default()); - let profiles_dir = std::env::temp_dir().join(format!("clash-verge-profile-cleanup-{}", nanoid::nanoid!())); - tokio::fs::create_dir_all(&profiles_dir).await?; - let active_profile = profiles_dir.join("Ractive.yaml"); - tokio::fs::write(&active_profile, "proxies: []").await?; - - Config::ensure_default_profile_items_for(&profiles).await?; - profiles.data_arc().cleanup_orphaned_files_in(&profiles_dir).await?; - - let profile_was_preserved = tokio::fs::try_exists(&active_profile).await?; - tokio::fs::remove_dir_all(&profiles_dir).await?; - - assert!(profile_was_preserved); - assert!(profiles.data_arc().items.is_none()); - Ok(()) - } -} diff --git a/src-tauri/src/config/profiles.rs b/src-tauri/src/config/profiles.rs index 6cf5754be..e65545c97 100644 --- a/src-tauri/src/config/profiles.rs +++ b/src-tauri/src/config/profiles.rs @@ -30,11 +30,6 @@ use tokio::{fs, task::JoinHandle}; pub(crate) static PROFILE_WRITE_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(()); -/// Matches generated profile files such as `R12345678.yaml` or `s12345678.js`. -#[allow(clippy::unwrap_used)] -static REGEX_PROFILE_FILE: LazyLock = - LazyLock::new(|| regex::Regex::new(r"^(?:[RLmrpg][a-zA-Z0-9]+\.yaml|s[a-zA-Z0-9]+\.js)$").unwrap()); - static ACTIVATE_SELECTED_TASK: LazyLock>>> = LazyLock::new(|| Mutex::new(None)); static ACTIVATE_SELECTED_GENERATION: AtomicU64 = AtomicU64::new(0); @@ -404,148 +399,6 @@ impl IProfiles { .collect() }) } - - /// 以 app 中的 profile 列表为准,删除不再需要的文件 - pub async fn cleanup_orphaned_files(&self) -> Result<()> { - let profiles_dir = dirs::app_profiles_dir()?; - self.cleanup_orphaned_files_in(&profiles_dir).await - } - - pub(super) async fn cleanup_orphaned_files_in(&self, profiles_dir: &Path) -> Result<()> { - if !profiles_dir.exists() { - return Ok(()); - } - - // An empty list may mean loading failed, so it is not safe evidence that every file is orphaned. - // https://github.com/clash-verge-rev/clash-verge-rev/issues/7577 - if self.items.as_ref().is_none_or(|v| v.is_empty()) { - logging!( - warn, - Type::Config, - "Profile items 为空,跳过孤儿文件清理以避免误删活跃的配置文件" - ); - return Ok(()); - } - - let active_files = self.get_all_active_files(); - - let protected_files = self.get_protected_global_files(); - - let mut total_files = 0; - let mut deleted_files = 0; - let mut failed_deletions = 0; - - let mut dir_entries = tokio::fs::read_dir(&profiles_dir).await?; - while let Some(entry) = dir_entries.next_entry().await? { - let path = entry.path(); - - if !path.is_file() { - continue; - } - - total_files += 1; - - if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) - && Self::is_profile_file(file_name) - { - if protected_files.contains(file_name) { - logging!(debug, Type::Config, "保护全局扩展配置文件: {file_name}"); - continue; - } - - if !active_files.contains(file_name) { - match path.to_path_buf().remove_if_exists().await { - Ok(_) => { - deleted_files += 1; - logging!(debug, Type::Config, "已清理冗余文件: {file_name}"); - } - Err(e) => { - failed_deletions += 1; - logging!(warn, Type::Config, "Warning: 清理文件失败: {file_name} - {e}"); - } - } - } - } - } - - logging!( - info, - Type::Config, - "Profile 文件清理完成: 总文件数={}, 删除文件数={}, 失败数={}", - total_files, - deleted_files, - failed_deletions - ); - - Ok(()) - } - - fn get_protected_global_files(&self) -> HashSet { - let mut protected_files = HashSet::new(); - - protected_files.insert("Merge.yaml".into()); - protected_files.insert("Script.js".into()); - - protected_files - } - - fn get_all_active_files(&self) -> HashSet<&str> { - let mut active_files: HashSet<&str> = HashSet::new(); - - if let Some(items) = &self.items { - for item in items { - if let Some(file) = &item.file { - active_files.insert(file); - } - - if let Some(itype) = &item.itype - && (itype == "remote" || itype == "local") - && let Some(option) = &item.option - { - if let Some(merge_uid) = &option.merge - && let Ok(merge_item) = self.get_item(merge_uid) - && let Some(file) = &merge_item.file - { - active_files.insert(file); - } - - if let Some(script_uid) = &option.script - && let Ok(script_item) = self.get_item(script_uid) - && let Some(file) = &script_item.file - { - active_files.insert(file); - } - - if let Some(rules_uid) = &option.rules - && let Ok(rules_item) = self.get_item(rules_uid) - && let Some(file) = &rules_item.file - { - active_files.insert(file); - } - - if let Some(proxies_uid) = &option.proxies - && let Ok(proxies_item) = self.get_item(proxies_uid) - && let Some(file) = &proxies_item.file - { - active_files.insert(file); - } - - if let Some(groups_uid) = &option.groups - && let Ok(groups_item) = self.get_item(groups_uid) - && let Some(file) = &groups_item.file - { - active_files.insert(file); - } - } - } - } - - active_files - } - - fn is_profile_file(filename: &str) -> bool { - REGEX_PROFILE_FILE.is_match(filename) - } } // These helpers serialize asynchronous operations against committed profile data.