diff --git a/src-tauri/src/cmd/profile.rs b/src-tauri/src/cmd/profile.rs index d4950b11a..60bdb09c1 100644 --- a/src-tauri/src/cmd/profile.rs +++ b/src-tauri/src/cmd/profile.rs @@ -117,7 +117,7 @@ pub async fn import_profile(url: std::string::String, option: Option) pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult { match profiles_reorder_safe(&active_id, &over_id).await { Ok(_) => { - logging!(debug, Type::Cmd, "重新排序配置文件"); + logging!(info, Type::Cmd, "重新排序配置文件"); Config::profiles().await.apply(); Ok(()) } diff --git a/src-tauri/src/config/prfitem.rs b/src-tauri/src/config/prfitem.rs index b06060b51..24dc7eb77 100644 --- a/src-tauri/src/config/prfitem.rs +++ b/src-tauri/src/config/prfitem.rs @@ -60,13 +60,13 @@ pub struct PrfItem { pub file_data: Option, } -#[derive(Default, Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Default, Debug, Clone, Deserialize, Serialize)] pub struct PrfSelected { pub name: Option, pub now: Option, } -#[derive(Default, Debug, Clone, Copy, PartialEq, Deserialize, Serialize)] +#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)] pub struct PrfExtra { pub upload: u64, pub download: u64, @@ -124,22 +124,25 @@ pub struct PrfOption { impl PrfOption { pub fn merge(one: Option<&Self>, other: Option<&Self>) -> Option { match (one, other) { - (Some(a), Some(b)) => Some(Self { - user_agent: b.user_agent.as_ref().or(a.user_agent.as_ref()).cloned(), - with_proxy: b.with_proxy.or(a.with_proxy), - self_proxy: b.self_proxy.or(a.self_proxy), - danger_accept_invalid_certs: b.danger_accept_invalid_certs.or(a.danger_accept_invalid_certs), - allow_auto_update: b.allow_auto_update.or(a.allow_auto_update), - update_interval: b.update_interval.or(a.update_interval), - merge: b.merge.as_ref().or(a.merge.as_ref()).cloned(), - script: b.script.as_ref().or(a.script.as_ref()).cloned(), - rules: b.rules.as_ref().or(a.rules.as_ref()).cloned(), - proxies: b.proxies.as_ref().or(a.proxies.as_ref()).cloned(), - groups: b.groups.as_ref().or(a.groups.as_ref()).cloned(), - timeout_seconds: b.timeout_seconds.or(a.timeout_seconds), - }), - (Some(a), None) => Some(a.clone()), - (None, Some(b)) => Some(b.clone()), + (Some(a_ref), Some(b_ref)) => { + let mut result = a_ref.clone(); + result.user_agent = b_ref.user_agent.clone().or(result.user_agent); + result.with_proxy = b_ref.with_proxy.or(result.with_proxy); + result.self_proxy = b_ref.self_proxy.or(result.self_proxy); + result.danger_accept_invalid_certs = + b_ref.danger_accept_invalid_certs.or(result.danger_accept_invalid_certs); + result.allow_auto_update = b_ref.allow_auto_update.or(result.allow_auto_update); + result.update_interval = b_ref.update_interval.or(result.update_interval); + result.merge = b_ref.merge.clone().or(result.merge); + result.script = b_ref.script.clone().or(result.script); + result.rules = b_ref.rules.clone().or(result.rules); + result.proxies = b_ref.proxies.clone().or(result.proxies); + result.groups = b_ref.groups.clone().or(result.groups); + result.timeout_seconds = b_ref.timeout_seconds.or(result.timeout_seconds); + Some(result) + } + (Some(a_ref), None) => Some(a_ref.clone()), + (None, Some(b_ref)) => Some(b_ref.clone()), (None, None) => None, } } diff --git a/src-tauri/src/config/profiles.rs b/src-tauri/src/config/profiles.rs index 43887b027..498a63047 100644 --- a/src-tauri/src/config/profiles.rs +++ b/src-tauri/src/config/profiles.rs @@ -5,16 +5,12 @@ use crate::utils::{ }; use anyhow::{Context as _, Result, bail}; use clash_verge_logging::{Type, logging}; -use once_cell::sync::OnceCell; -use regex::Regex; use serde::{Deserialize, Serialize}; use serde_yaml_ng::Mapping; use smartstring::alias::String; -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; use tokio::fs; -static PROFILE_FILE_RE: OnceCell = OnceCell::new(); - /// Define the `profiles.yaml` schema #[derive(Default, Debug, Clone, Deserialize, Serialize)] pub struct IProfiles { @@ -41,15 +37,23 @@ pub struct CleanupResult { macro_rules! patch { ($lv: expr, $rv: expr, $key: tt) => { - if let Some(ref val) = $rv.$key { - if Some(val) != $lv.$key.as_ref() { - $lv.$key = Some(val.to_owned()); - } + if ($rv.$key).is_some() { + $lv.$key = $rv.$key.to_owned(); } }; } impl IProfiles { + // Helper to find and remove an item by uid from the items vec, returning its file name (if any). + fn take_item_file_by_uid(items: &mut Vec, target_uid: Option) -> Option { + for (i, _) in items.iter().enumerate() { + if items[i].uid == target_uid { + return items.remove(i).file; + } + } + None + } + pub async fn new() -> Self { let path = match dirs::profiles_path() { Ok(p) => p, @@ -59,22 +63,21 @@ impl IProfiles { } }; - let mut profiles = match help::read_yaml::(&path).await { - Ok(profiles) => profiles, + match help::read_yaml::(&path).await { + Ok(mut profiles) => { + let items = profiles.items.get_or_insert_with(Vec::new); + for item in items.iter_mut() { + if item.uid.is_none() { + item.uid = Some(help::get_uid("d").into()); + } + } + profiles + } Err(err) => { logging!(error, Type::Config, "{err}"); - return Self::default(); - } - }; - - let items = profiles.items.get_or_insert_with(Vec::new); - for item in items.iter_mut() { - if item.uid.is_none() { - item.uid = Some(help::get_uid("d").into()); + Self::default() } } - - profiles } pub async fn save_file(&self) -> Result<()> { @@ -110,28 +113,38 @@ impl IProfiles { pub fn get_item(&self, uid: impl AsRef) -> Result<&PrfItem> { let uid_str = uid.as_ref(); - self.items - .as_ref() - .ok_or_else(|| anyhow::anyhow!("no profile items found"))? - .iter() - .find(|each| each.uid.as_ref().is_some_and(|uid_val| uid_val.as_str() == uid_str)) - .ok_or_else(|| anyhow::anyhow!("failed to get the profile item \"uid:{}\"", uid_str)) + if let Some(items) = self.items.as_ref() { + for each in items.iter() { + if let Some(uid_val) = &each.uid + && uid_val.as_str() == uid_str + { + return Ok(each); + } + } + } + + bail!("failed to get the profile item \"uid:{}\"", uid_str); } /// append new item /// if the file_data is some /// then should save the data to file pub async fn append_item(&mut self, item: &mut PrfItem) -> Result<()> { - anyhow::ensure!(item.uid.is_some(), "the uid should not be null"); + let uid = &item.uid; + if uid.is_none() { + bail!("the uid should not be null"); + } // save the file data // move the field value after save if let Some(file_data) = item.file_data.take() { - anyhow::ensure!(item.file.is_some(), "the file should not be null"); + if item.file.is_none() { + bail!("the file should not be null"); + } let file = item .file - .as_ref() + .clone() .ok_or_else(|| anyhow::anyhow!("file field is required when file_data is provided"))?; let path = dirs::app_profiles_dir()?.join(file.as_str()); @@ -140,116 +153,111 @@ impl IProfiles { .with_context(|| format!("failed to write to file \"{file}\""))?; } - if self.current.is_none() - && let Some(t) = item.itype.as_deref() - && (t == "remote" || t == "local") - { - self.current = item.uid.to_owned(); + if self.current.is_none() && (item.itype == Some("remote".into()) || item.itype == Some("local".into())) { + self.current = uid.to_owned(); } - self.items.get_or_insert_default().push(std::mem::take(item)); + if self.items.is_none() { + self.items = Some(vec![]); + } + + if let Some(items) = self.items.as_mut() { + items.push(item.to_owned()); + } Ok(()) } /// reorder items - pub async fn reorder(&mut self, active_id: &str, over_id: &str) -> Result<()> { - if active_id == over_id { - return Ok(()); + pub async fn reorder(&mut self, active_id: &String, over_id: &String) -> Result<()> { + let mut items = self.items.take().unwrap_or_default(); + let mut old_index = None; + let mut new_index = None; + + for (i, _) in items.iter().enumerate() { + if items[i].uid.as_ref() == Some(active_id) { + old_index = Some(i); + } + if items[i].uid.as_ref() == Some(over_id) { + new_index = Some(i); + } } - let Some(items) = self.items.as_mut() else { - return Ok(()); + let (old_idx, new_idx) = match (old_index, new_index) { + (Some(old), Some(new)) => (old, new), + _ => return Ok(()), }; - - let mut old_idx = None; - let mut new_idx = None; - - for (i, item) in items.iter().enumerate() { - if let Some(uid) = item.uid.as_ref() { - if uid == active_id { - old_idx = Some(i); - } - if uid == over_id { - new_idx = Some(i); - } - } - if old_idx.is_some() && new_idx.is_some() { - break; - } - } - - if let (Some(old), Some(new)) = (old_idx, new_idx) { - if old < new { - items[old..=new].rotate_left(1); - } else { - items[new..=old].rotate_right(1); - } - - return self.save_file().await; - } - - Ok(()) + let item = items.remove(old_idx); + items.insert(new_idx, item); + self.items = Some(items); + self.save_file().await } /// update the item value pub async fn patch_item(&mut self, uid: &String, item: &PrfItem) -> Result<()> { - let items = self - .items - .as_mut() - .ok_or_else(|| anyhow::anyhow!("no profile items found"))?; + let mut items = self.items.take().unwrap_or_default(); - let target = items.iter_mut().find(|each| each.uid.as_ref() == Some(uid)); + for each in items.iter_mut() { + if each.uid.as_ref() == Some(uid) { + patch!(each, item, itype); + patch!(each, item, name); + patch!(each, item, desc); + patch!(each, item, file); + patch!(each, item, url); + patch!(each, item, selected); + patch!(each, item, extra); + patch!(each, item, updated); + patch!(each, item, option); - if let Some(each) = target { - patch!(each, item, itype); - patch!(each, item, name); - patch!(each, item, desc); - patch!(each, item, file); - patch!(each, item, url); - patch!(each, item, selected); - patch!(each, item, extra); - patch!(each, item, updated); - patch!(each, item, option); - - return self.save_file().await; + self.items = Some(items); + return self.save_file().await; + } } + self.items = Some(items); bail!("failed to find the profile item \"uid:{uid}\"") } /// be used to update the remote item /// only patch `updated` `extra` `file_data` pub async fn update_item(&mut self, uid: &String, item: &mut PrfItem) -> Result<()> { - let target = self - .items - .get_or_insert_default() - .iter_mut() - .find(|each| each.uid.as_ref() == Some(uid)) - .ok_or_else(|| anyhow::anyhow!("Item not found"))?; + if self.items.is_none() { + self.items = Some(vec![]); + } - target.extra = item.extra; - target.updated = item.updated; - target.home = std::mem::take(&mut item.home); - target.option = PrfOption::merge(target.option.as_ref(), item.option.as_ref()); + // find the item + let _ = self.get_item(uid)?; - let Some(file_data) = item.file_data.take() else { - return self.save_file().await; - }; + if let Some(items) = self.items.as_mut() { + let some_uid = Some(uid.clone()); - let file = target - .file - .take() - .or_else(|| item.file.take()) - .unwrap_or_else(|| format!("{}.yaml", uid).into()); + for each in items.iter_mut() { + if each.uid == some_uid { + each.extra = item.extra; + each.updated = item.updated; + each.home = item.home.to_owned(); + each.option = PrfOption::merge(each.option.as_ref(), item.option.as_ref()); + // save the file data + // move the field value after save + if let Some(file_data) = item.file_data.take() { + let file = each.file.take(); + let file = + file.unwrap_or_else(|| item.file.take().unwrap_or_else(|| format!("{}.yaml", &uid).into())); - let path = dirs::app_profiles_dir()?.join(file.as_str()); + // the file must exists + each.file = Some(file.clone()); - fs::write(&path, file_data.as_bytes()) - .await - .with_context(|| format!("failed to write to file \"{file}\""))?; + let path = dirs::app_profiles_dir()?.join(file.as_str()); - target.file = Some(file); + fs::write(&path, file_data.as_bytes()) + .await + .with_context(|| format!("failed to write to file \"{file}\""))?; + } + + break; + } + } + } self.save_file().await } @@ -257,82 +265,68 @@ impl IProfiles { /// delete item /// if delete the current then return true pub async fn delete_item(&mut self, uid: &String) -> Result { - let uids_to_remove: HashSet = { - let item = self.get_item(uid)?; - let mut set = HashSet::new(); - set.insert(uid.clone()); - - if let Some(opt) = &item.option { - if let Some(u) = &opt.merge { - set.insert(u.clone()); - } - if let Some(u) = &opt.script { - set.insert(u.clone()); - } - if let Some(u) = &opt.rules { - set.insert(u.clone()); - } - if let Some(u) = &opt.proxies { - set.insert(u.clone()); - } - if let Some(u) = &opt.groups { - set.insert(u.clone()); - } - } - set - }; - + let current = self.current.as_ref().unwrap_or(uid); + let current = current.clone(); + let item = self.get_item(uid)?; + let merge_uid = item.option.as_ref().and_then(|e| e.merge.clone()); + let script_uid = item.option.as_ref().and_then(|e| e.script.clone()); + let rules_uid = item.option.as_ref().and_then(|e| e.rules.clone()); + let proxies_uid = item.option.as_ref().and_then(|e| e.proxies.clone()); + let groups_uid = item.option.as_ref().and_then(|e| e.groups.clone()); let mut items = self.items.take().unwrap_or_default(); - let mut deleted_files = Vec::new(); - items.retain_mut(|item| { - if let Some(item_uid) = item.uid.as_ref() - && uids_to_remove.contains(item_uid) - { - if let Some(file) = item.file.take() { - deleted_files.push(file); + // remove the main item (if exists) and delete its file + if let Some(file) = Self::take_item_file_by_uid(&mut items, Some(uid.clone())) { + let _ = dirs::app_profiles_dir()?.join(file.as_str()).remove_if_exists().await; + } + + // remove related extension items (merge, script, rules, proxies, groups) + if let Some(file) = Self::take_item_file_by_uid(&mut items, merge_uid.clone()) { + let _ = dirs::app_profiles_dir()?.join(file.as_str()).remove_if_exists().await; + } + if let Some(file) = Self::take_item_file_by_uid(&mut items, script_uid.clone()) { + let _ = dirs::app_profiles_dir()?.join(file.as_str()).remove_if_exists().await; + } + if let Some(file) = Self::take_item_file_by_uid(&mut items, rules_uid.clone()) { + let _ = dirs::app_profiles_dir()?.join(file.as_str()).remove_if_exists().await; + } + if let Some(file) = Self::take_item_file_by_uid(&mut items, proxies_uid.clone()) { + let _ = dirs::app_profiles_dir()?.join(file.as_str()).remove_if_exists().await; + } + if let Some(file) = Self::take_item_file_by_uid(&mut items, groups_uid.clone()) { + let _ = dirs::app_profiles_dir()?.join(file.as_str()).remove_if_exists().await; + } + // delete the original uid + if current == *uid { + self.current = None; + for item in items.iter() { + if item.itype == Some("remote".into()) || item.itype == Some("local".into()) { + self.current = item.uid.clone(); + break; } - return false; } - true - }); - - let is_deleting_current = self.current.as_ref() == Some(uid); - if is_deleting_current { - self.current = items - .iter() - .find(|i| i.itype.as_deref() == Some("remote") || i.itype.as_deref() == Some("local")) - .and_then(|i| i.uid.clone()); } self.items = Some(items); - - if let Ok(profile_dir) = dirs::app_profiles_dir() { - for file in deleted_files { - let _ = profile_dir.join(file.as_str()).remove_if_exists().await; - } - } - self.save_file().await?; - Ok(is_deleting_current) + Ok(current == *uid) } /// 获取current指向的订阅内容 pub async fn current_mapping(&self) -> Result { - let (Some(current), Some(items)) = (self.current.as_ref(), self.items.as_ref()) else { - return Ok(Mapping::new()); - }; - - let Some(target) = items.iter().find(|e| e.uid.as_ref() == Some(current)) else { - bail!("failed to find the current profile \"uid:{current}\""); - }; - - let file = target - .file - .as_ref() - .ok_or_else(|| anyhow::anyhow!("failed to get the file field"))?; - let file_path = dirs::app_profiles_dir()?.join(file.as_str()); - help::read_mapping(&file_path).await + match (self.current.as_ref(), self.items.as_ref()) { + (Some(current), Some(items)) => { + if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) { + let file_path = match item.file.as_ref() { + Some(file) => dirs::app_profiles_dir()?.join(file.as_str()), + None => bail!("failed to get the file field"), + }; + return help::read_mapping(&file_path).await; + } + bail!("failed to find the current profile \"uid:{current}\""); + } + _ => Ok(Mapping::new()), + } } /// 判断profile是否是current指向的 @@ -342,32 +336,32 @@ impl IProfiles { /// 获取所有的profiles(uid,名称, 是否为 current) pub fn profiles_preview(&self) -> Option>> { - let items = self.items.as_ref()?; - let current_uid = self.current.as_ref(); - - let previews = items - .iter() - .filter_map(|e| { - let uid = e.uid.as_ref()?; - let name = e.name.as_ref()?; - Some(IProfilePreview { - uid, - name, - is_current: current_uid == Some(uid), + self.items.as_ref().map(|items| { + items + .iter() + .filter_map(|e| { + if let (Some(uid), Some(name)) = (e.uid.as_ref(), e.name.as_ref()) { + let is_current = self.is_current_profile_index(uid); + let preview = IProfilePreview { uid, name, is_current }; + Some(preview) + } else { + None + } }) - }) - .collect(); - - Some(previews) + .collect() + }) } /// 通过 uid 获取名称 - pub fn get_name_by_uid(&self, uid: &str) -> Option<&String> { - self.items - .as_ref()? - .iter() - .find(|item| item.uid.as_deref() == Some(uid)) - .and_then(|item| item.name.as_ref()) + pub fn get_name_by_uid(&self, uid: &String) -> Option<&String> { + if let Some(items) = &self.items { + for item in items { + if item.uid.as_ref() == Some(uid) { + return item.name.as_ref(); + } + } + } + None } /// 以 app 中的 profile 列表为准,删除不再需要的文件 @@ -454,39 +448,59 @@ impl IProfiles { /// 获取所有 active profile 关联的文件名 fn get_all_active_files(&self) -> HashSet<&str> { - let mut active_files = HashSet::new(); - let items = match &self.items { - Some(i) => i, - None => return active_files, - }; + let mut active_files: HashSet<&str> = HashSet::new(); - let item_map: HashMap, &PrfItem> = items.iter().map(|i| (i.uid.as_deref(), i)).collect(); + if let Some(items) = &self.items { + for item in items { + // 收集所有类型 profile 的文件 + if let Some(file) = &item.file { + active_files.insert(file); + } - for item in items { - if let Some(f) = &item.file { - active_files.insert(f.as_str()); - } - - let Some(opt) = &item.option else { - continue; - }; - - let related = [ - opt.merge.as_deref(), - opt.script.as_deref(), - opt.rules.as_deref(), - opt.proxies.as_deref(), - opt.groups.as_deref(), - ]; - - for r_uid in related.into_iter().flatten() { - if let Some(r_item) = item_map.get(&Some(r_uid)) - && let Some(f) = &r_item.file + // 对于主 profile 类型(remote/local),还需要收集其关联的扩展文件 + if let Some(itype) = &item.itype + && (itype == "remote" || itype == "local") + && let Some(option) = &item.option { - active_files.insert(f.as_str()); + // 收集关联的扩展文件 + 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 } @@ -501,10 +515,18 @@ impl IProfiles { // p12345678.yaml (proxies) // g12345678.yaml (groups) - #[allow(clippy::unwrap_used)] - let re = PROFILE_FILE_RE - .get_or_init(|| Regex::new(r"^(?:[RLmprg][a-zA-Z0-9_-]+\.yaml|s[a-zA-Z0-9_-]+\.js)$").unwrap()); - re.is_match(filename) + let patterns = [ + r"^[RL][a-zA-Z0-9]+\.yaml$", // Remote/Local profiles + r"^m[a-zA-Z0-9]+\.yaml$", // Merge files + r"^s[a-zA-Z0-9]+\.js$", // Script files + r"^[rpg][a-zA-Z0-9]+\.yaml$", // Rules/Proxies/Groups files + ]; + + patterns.iter().any(|pattern| { + regex::Regex::new(pattern) + .map(|re| re.is_match(filename)) + .unwrap_or(false) + }) } } diff --git a/src-tauri/src/core/timer.rs b/src-tauri/src/core/timer.rs index 0b7dc6545..5a00b36a9 100644 --- a/src-tauri/src/core/timer.rs +++ b/src-tauri/src/core/timer.rs @@ -25,7 +25,6 @@ pub struct TimerTask { pub last_run: i64, // Timestamp of last execution } -// TODO 一个 Timer 负责轻量, 一个 Timer 负责订阅更新。当前会生产 N(订阅数量) + 1 个定时任务 pub struct Timer { /// cron manager pub delay_timer: Arc>,