use super::{prfitem::PrfItem, ChainItem, PrfOption}; use crate::{ core::CoreManager, utils::{config, dirs, help}, }; use anyhow::{bail, Context, Result}; use once_cell::sync::OnceCell; use parking_lot::Mutex; use serde::{Deserialize, Serialize}; use serde_yaml::Mapping; use std::collections::HashMap; use std::sync::Arc; use std::{fs, io::Write}; pub struct ProfilesN { pub config: Arc>, } impl ProfilesN { pub fn global() -> &'static ProfilesN { static PROFILES: OnceCell = OnceCell::new(); PROFILES.get_or_init(|| ProfilesN { config: Arc::new(Mutex::new(IProfiles::read_file())), }) } /// 更新单个配置 pub async fn update_item(&self, uid: String, option: Option) -> Result<()> { let (url, opt) = { let profiles = self.config.lock(); let item = profiles.get_item(&uid)?; if let Some(typ) = item.itype.as_ref() { // maybe only valid for `local` profile if *typ != "remote" { // reactivate the config if Some(uid) == profiles.get_current() { drop(profiles); tauri::async_runtime::block_on(async { CoreManager::global().activate_config().await })?; } return Ok(()); } } if item.url.is_none() { bail!("failed to get the profile item url"); } (item.url.clone().unwrap(), item.option.clone()) }; let merged_opt = PrfOption::merge(opt, option); let item = PrfItem::from_url(&url, None, None, merged_opt).await?; let mut profiles = self.config.lock(); profiles.update_item(uid.clone(), item)?; // reactivate the profile if Some(uid) == profiles.get_current() { drop(profiles); tauri::async_runtime::block_on(async { CoreManager::global().activate_config().await })?; } Ok(()) } } /// Define the `profiles.yaml` schema #[derive(Default, Debug, Clone, Deserialize, Serialize)] pub struct IProfiles { /// same as PrfConfig.current current: Option, /// same as PrfConfig.chain chain: Option>, /// record valid fields for clash valid: Option>, /// profile list items: Option>, } macro_rules! patch { ($lv: expr, $rv: expr, $key: tt) => { if ($rv.$key).is_some() { $lv.$key = $rv.$key; } }; } impl IProfiles { /// read the config from the file pub fn read_file() -> Self { let mut profiles = config::read_yaml::(dirs::profiles_path()); if profiles.items.is_none() { profiles.items = Some(vec![]); } // compatible with the old old old version profiles.items.as_mut().map(|items| { for mut item in items.iter_mut() { if item.uid.is_none() { item.uid = Some(help::get_uid("d")); } } }); profiles } /// save the config to the file pub fn save_file(&self) -> Result<()> { config::save_yaml( dirs::profiles_path(), self, Some("# Profiles Config for Clash Verge\n\n"), ) } /// get the current uid pub fn get_current(&self) -> Option { self.current.clone() } /// only change the main to the target id pub fn put_current(&mut self, uid: String) -> Result<()> { if self.items.is_none() { self.items = Some(vec![]); } let items = self.items.as_ref().unwrap(); let some_uid = Some(uid.clone()); if items.iter().find(|&each| each.uid == some_uid).is_some() { self.current = some_uid; return self.save_file(); } bail!("invalid uid \"{uid}\""); } /// just change the `chain` pub fn put_chain(&mut self, chain: Option>) -> Result<()> { self.chain = chain; self.save_file() } /// just change the `field` pub fn put_valid(&mut self, valid: Option>) -> Result<()> { self.valid = valid; self.save_file() } /// get items ref pub fn get_items(&self) -> Option<&Vec> { self.items.as_ref() } /// find the item by the uid pub fn get_item(&self, uid: &String) -> Result<&PrfItem> { if self.items.is_some() { let items = self.items.as_ref().unwrap(); let some_uid = Some(uid.clone()); for each in items.iter() { if each.uid == some_uid { return Ok(each); } } } bail!("failed to get the profile item \"uid:{uid}\""); } /// append new item /// if the file_data is some /// then should save the data to file pub fn append_item(&mut self, mut item: PrfItem) -> Result<()> { if item.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() { if item.file.is_none() { bail!("the file should not be null"); } let file = item.file.clone().unwrap(); let path = dirs::app_profiles_dir().join(&file); fs::File::create(path) .context(format!("failed to create file \"{}\"", file))? .write(file_data.as_bytes()) .context(format!("failed to write to file \"{}\"", file))?; } if self.items.is_none() { self.items = Some(vec![]); } self.items.as_mut().map(|items| items.push(item)); self.save_file() } /// update the item value pub fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> { let mut items = self.items.take().unwrap_or(vec![]); for mut each in items.iter_mut() { if each.uid == Some(uid.clone()) { 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); self.items = Some(items); return self.save_file(); } } 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 fn update_item(&mut self, uid: String, mut item: PrfItem) -> Result<()> { if self.items.is_none() { self.items = Some(vec![]); } // find the item let _ = self.get_item(&uid)?; if let Some(items) = self.items.as_mut() { let some_uid = Some(uid.clone()); for mut each in items.iter_mut() { if each.uid == some_uid { each.extra = item.extra; each.updated = item.updated; // 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(item.file.take().unwrap_or(format!("{}.yaml", &uid))); // the file must exists each.file = Some(file.clone()); let path = dirs::app_profiles_dir().join(&file); fs::File::create(path) .context(format!("failed to create file \"{}\"", file))? .write(file_data.as_bytes()) .context(format!("failed to write to file \"{}\"", file))?; } break; } } } self.save_file() } /// delete item /// if delete the current then return true pub fn delete_item(&mut self, uid: String) -> Result { let current = self.current.as_ref().unwrap_or(&uid); let current = current.clone(); let mut items = self.items.take().unwrap_or(vec![]); let mut index = None; // get the index for i in 0..items.len() { if items[i].uid == Some(uid.clone()) { index = Some(i); break; } } if let Some(index) = index { items.remove(index).file.map(|file| { let path = dirs::app_profiles_dir().join(file); if path.exists() { let _ = fs::remove_file(path); } }); } // delete the original uid if current == uid { self.current = match items.len() > 0 { true => items[0].uid.clone(), false => None, }; } self.items = Some(items); self.save_file()?; Ok(current == uid) } /// generate the current Mapping data fn gen_current(&self) -> Result { let config = Mapping::new(); if self.current.is_none() || self.items.is_none() { return Ok(config); } let current = self.current.clone().unwrap(); for item in self.items.as_ref().unwrap().iter() { if item.uid == Some(current.clone()) { let file_path = match item.file.clone() { Some(file) => dirs::app_profiles_dir().join(file), None => bail!("failed to get the file field"), }; if !file_path.exists() { bail!("failed to read the file \"{}\"", file_path.display()); } return Ok(config::read_merge_mapping(file_path.clone())); } } bail!("failed to find current profile \"uid:{current}\""); } /// generate the data for activate clash config pub fn gen_activate(&self) -> Result { let current = self.gen_current()?; let chain = match self.chain.as_ref() { Some(chain) => chain .iter() .filter_map(|uid| self.get_item(uid).ok()) .filter_map(|item| item.to_enhance()) .collect::>(), None => vec![], }; let valid = self.valid.clone().unwrap_or(vec![]); Ok(PrfActivate { current, chain, valid, }) } } #[derive(Default, Clone)] pub struct PrfActivate { pub current: Mapping, pub chain: Vec, pub valid: Vec, } #[derive(Default, Debug, Clone, Deserialize, Serialize)] pub struct RuntimeResult { pub config: Option, pub config_yaml: Option, // 记录在配置中(包括merge和script生成的)出现过的keys // 这些keys不一定都生效 pub exists_keys: Vec, pub chain_logs: HashMap>, }