From 749df8922993364a5321d1e9fb8e22bee5c2aaa8 Mon Sep 17 00:00:00 2001 From: GyDi Date: Tue, 1 Mar 2022 08:58:47 +0800 Subject: [PATCH] refactor: profile config --- src-tauri/src/cmds.rs | 118 ++--- src-tauri/src/core/clash.rs | 59 ++- src-tauri/src/core/mod.rs | 1 - src-tauri/src/core/prfitem.rs | 332 ------------- src-tauri/src/core/profiles.rs | 633 +++++++++++++----------- src-tauri/src/utils/help.rs | 86 ++++ src-tauri/src/utils/init.rs | 12 +- src-tauri/src/utils/mod.rs | 3 +- src-tauri/src/utils/resolve.rs | 14 +- src-tauri/src/utils/tmpl.rs | 4 +- src/components/profile/profile-item.tsx | 10 +- src/components/proxy/proxy-group.tsx | 9 +- src/pages/profiles.tsx | 30 +- src/services/cmds.ts | 10 +- src/services/types.ts | 5 +- 15 files changed, 567 insertions(+), 759 deletions(-) delete mode 100644 src-tauri/src/core/prfitem.rs create mode 100644 src-tauri/src/utils/help.rs diff --git a/src-tauri/src/cmds.rs b/src-tauri/src/cmds.rs index 8eb1609..4026c22 100644 --- a/src-tauri/src/cmds.rs +++ b/src-tauri/src/cmds.rs @@ -1,37 +1,18 @@ use crate::{ - core::{ClashInfo, ProfileItem, Profiles, VergeConfig}, + core::{ClashInfo, PrfItem, Profiles, VergeConfig}, + ret_err, states::{ClashState, ProfilesState, VergeState}, - utils::{dirs, fetch::fetch_profile, sysopt::SysProxyConfig}, + utils::{dirs, sysopt::SysProxyConfig}, + wrap_err, }; use anyhow::Result; use serde_yaml::Mapping; use std::{path::PathBuf, process::Command}; use tauri::{api, State}; -/// wrap the anyhow error -/// transform the error to String -macro_rules! wrap_err { - ($stat: expr) => { - match $stat { - Ok(a) => Ok(a), - Err(err) => { - log::error!("{}", err.to_string()); - Err(format!("{}", err.to_string())) - } - } - }; -} - -/// return the string literal error -macro_rules! ret_err { - ($str: literal) => { - return Err($str.into()) - }; -} - /// get all profiles from `profiles.yaml` #[tauri::command] -pub fn get_profiles(profiles_state: State<'_, ProfilesState>) -> Result { +pub fn get_profiles<'a>(profiles_state: State<'_, ProfilesState>) -> Result { let profiles = profiles_state.0.lock().unwrap(); Ok(profiles.clone()) } @@ -51,9 +32,10 @@ pub async fn import_profile( with_proxy: bool, profiles_state: State<'_, ProfilesState>, ) -> Result<(), String> { - let result = fetch_profile(&url, with_proxy).await?; + let item = wrap_err!(PrfItem::from_url(&url, with_proxy).await)?; + let mut profiles = profiles_state.0.lock().unwrap(); - wrap_err!(profiles.import_from_url(url, result)) + wrap_err!(profiles.append_item(item)) } /// new a profile @@ -65,59 +47,50 @@ pub async fn new_profile( desc: String, profiles_state: State<'_, ProfilesState>, ) -> Result<(), String> { + let item = wrap_err!(PrfItem::from_local(name, desc))?; let mut profiles = profiles_state.0.lock().unwrap(); - wrap_err!(profiles.append_item(name, desc))?; - Ok(()) + + wrap_err!(profiles.append_item(item)) } /// Update the profile #[tauri::command] pub async fn update_profile( - index: usize, + index: String, with_proxy: bool, clash_state: State<'_, ClashState>, profiles_state: State<'_, ProfilesState>, ) -> Result<(), String> { - // maybe we can get the url from the web app directly - let url = match profiles_state.0.lock() { - Ok(mut profile) => { - let items = profile.items.take().unwrap_or(vec![]); - if index >= items.len() { - ret_err!("the index out of bound"); - } - let url = match &items[index].url { - Some(u) => u.clone(), - None => ret_err!("failed to update profile for `invalid url`"), - }; - profile.items = Some(items); - url + let url = { + // must release the lock here + let profiles = profiles_state.0.lock().unwrap(); + let item = wrap_err!(profiles.get_item(&index))?; + + if item.url.is_none() { + ret_err!("failed to get the item url"); } - Err(_) => ret_err!("failed to get profiles lock"), + + item.url.clone().unwrap() }; - let result = fetch_profile(&url, with_proxy).await?; + let item = wrap_err!(PrfItem::from_url(&url, with_proxy).await)?; - match profiles_state.0.lock() { - Ok(mut profiles) => { - wrap_err!(profiles.update_item(index, result))?; + let mut profiles = profiles_state.0.lock().unwrap(); + wrap_err!(profiles.update_item(index.clone(), item))?; - // reactivate the profile - let current = profiles.current.clone().unwrap_or(0); - if current == index { - let clash = clash_state.0.lock().unwrap(); - wrap_err!(profiles.activate(&clash)) - } else { - Ok(()) - } - } - Err(_) => ret_err!("failed to get profiles lock"), + // reactivate the profile + if Some(index) == profiles.get_current() { + let clash = clash_state.0.lock().unwrap(); + wrap_err!(clash.activate(&profiles))?; } + + Ok(()) } /// change the current profile #[tauri::command] pub fn select_profile( - index: usize, + index: String, clash_state: State<'_, ClashState>, profiles_state: State<'_, ProfilesState>, ) -> Result<(), String> { @@ -125,13 +98,13 @@ pub fn select_profile( wrap_err!(profiles.put_current(index))?; let clash = clash_state.0.lock().unwrap(); - wrap_err!(profiles.activate(&clash)) + wrap_err!(clash.activate(&profiles)) } /// delete profile item #[tauri::command] pub fn delete_profile( - index: usize, + index: String, clash_state: State<'_, ClashState>, profiles_state: State<'_, ProfilesState>, ) -> Result<(), String> { @@ -139,7 +112,7 @@ pub fn delete_profile( if wrap_err!(profiles.delete_item(index))? { let clash = clash_state.0.lock().unwrap(); - wrap_err!(profiles.activate(&clash))?; + wrap_err!(clash.activate(&profiles))?; } Ok(()) @@ -148,8 +121,8 @@ pub fn delete_profile( /// patch the profile config #[tauri::command] pub fn patch_profile( - index: usize, - profile: ProfileItem, + index: String, + profile: PrfItem, profiles_state: State<'_, ProfilesState>, ) -> Result<(), String> { let mut profiles = profiles_state.0.lock().unwrap(); @@ -158,19 +131,16 @@ pub fn patch_profile( /// run vscode command to edit the profile #[tauri::command] -pub fn view_profile(index: usize, profiles_state: State<'_, ProfilesState>) -> Result<(), String> { - let mut profiles = profiles_state.0.lock().unwrap(); - let items = profiles.items.take().unwrap_or(vec![]); +pub fn view_profile(index: String, profiles_state: State<'_, ProfilesState>) -> Result<(), String> { + let profiles = profiles_state.0.lock().unwrap(); + let item = wrap_err!(profiles.get_item(&index))?; - if index >= items.len() { - profiles.items = Some(items); - ret_err!("the index out of bound"); + let file = item.file.clone(); + if file.is_none() { + ret_err!("the file is null"); } - let file = items[index].file.clone().unwrap_or("".into()); - profiles.items = Some(items); - - let path = dirs::app_profiles_dir().join(file); + let path = dirs::app_profiles_dir().join(file.unwrap()); if !path.exists() { ret_err!("the file not found"); } @@ -285,7 +255,7 @@ pub fn patch_verge_config( wrap_err!(clash.tun_mode(tun_mode.unwrap()))?; clash.update_config(); - wrap_err!(profiles.activate(&clash))?; + wrap_err!(clash.activate(&profiles))?; } Ok(()) diff --git a/src-tauri/src/core/clash.rs b/src-tauri/src/core/clash.rs index d6a1d8c..811890b 100644 --- a/src-tauri/src/core/clash.rs +++ b/src-tauri/src/core/clash.rs @@ -1,6 +1,9 @@ +use std::collections::HashMap; + use super::{Profiles, Verge}; use crate::utils::{config, dirs}; use anyhow::{bail, Result}; +use reqwest::header::HeaderMap; use serde::{Deserialize, Serialize}; use serde_yaml::{Mapping, Value}; use tauri::api::process::{Command, CommandChild, CommandEvent}; @@ -153,7 +156,7 @@ impl Clash { self.update_config(); self.drop_sidecar()?; self.run_sidecar()?; - profiles.activate(&self) + self.activate(profiles) } /// update the clash info @@ -191,11 +194,7 @@ impl Clash { verge.init_sysproxy(port); } - if self.config.contains_key(key) { - self.config[key] = value; - } else { - self.config.insert(key.clone(), value); - } + self.config.insert(key.clone(), value); } self.save_config() } @@ -241,6 +240,54 @@ impl Clash { self.save_config() } + + /// activate the profile + pub fn activate(&self, profiles: &Profiles) -> Result<()> { + let temp_path = dirs::profiles_temp_path(); + let info = self.info.clone(); + let mut config = self.config.clone(); + let gen_config = profiles.gen_activate()?; + + for (key, value) in gen_config.into_iter() { + config.insert(key, value); + } + + config::save_yaml(temp_path.clone(), &config, Some("# Clash Verge Temp File"))?; + + tauri::async_runtime::spawn(async move { + let server = info.server.clone().unwrap(); + let server = format!("http://{server}/configs"); + + let mut headers = HeaderMap::new(); + headers.insert("Content-Type", "application/json".parse().unwrap()); + + if let Some(secret) = info.secret.as_ref() { + let secret = format!("Bearer {}", secret.clone()).parse().unwrap(); + headers.insert("Authorization", secret); + } + + let mut data = HashMap::new(); + data.insert("path", temp_path.as_os_str().to_str().unwrap()); + + for _ in 0..5 { + match reqwest::ClientBuilder::new().no_proxy().build() { + Ok(client) => match client + .put(&server) + .headers(headers.clone()) + .json(&data) + .send() + .await + { + Ok(_) => break, + Err(err) => log::error!("failed to activate for `{err}`"), + }, + Err(err) => log::error!("failed to activate for `{err}`"), + } + } + }); + + Ok(()) + } } impl Default for Clash { diff --git a/src-tauri/src/core/mod.rs b/src-tauri/src/core/mod.rs index 1075ba5..1cf8f1e 100644 --- a/src-tauri/src/core/mod.rs +++ b/src-tauri/src/core/mod.rs @@ -1,5 +1,4 @@ mod clash; -mod prfitem; mod profiles; mod verge; diff --git a/src-tauri/src/core/prfitem.rs b/src-tauri/src/core/prfitem.rs deleted file mode 100644 index fa3be88..0000000 --- a/src-tauri/src/core/prfitem.rs +++ /dev/null @@ -1,332 +0,0 @@ -//! Todos -//! refactor the profiles - -use crate::utils::{config, dirs}; -use anyhow::{bail, Result}; -use serde::{Deserialize, Serialize}; -use serde_yaml::{Mapping, Value}; -use std::{fs, str::FromStr}; - -#[derive(Default, Debug, Clone, Deserialize, Serialize)] -pub struct PrfItem { - pub uid: Option, - - /// profile item type - /// enum value: remote | local | script | merge - #[serde(rename = "type")] - pub itype: Option, - - /// profile name - pub name: Option, - - /// profile description - #[serde(skip_serializing_if = "Option::is_none")] - pub desc: Option, - - /// profile file - pub file: Option, - - /// source url - #[serde(skip_serializing_if = "Option::is_none")] - pub url: Option, - - /// selected infomation - #[serde(skip_serializing_if = "Option::is_none")] - pub selected: Option>, - - /// user info - #[serde(skip_serializing_if = "Option::is_none")] - pub extra: Option, - - /// updated time - pub updated: Option, -} - -#[derive(Default, Debug, Clone, Deserialize, Serialize)] -pub struct PrfSelected { - pub name: Option, - pub now: Option, -} - -#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)] -pub struct PrfExtra { - pub upload: usize, - pub download: usize, - pub total: usize, - pub expire: usize, -} - -type FileData = String; - -impl PrfItem { - pub fn gen_now() -> usize { - use std::time::{SystemTime, UNIX_EPOCH}; - - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs() as _ - } - - /// generate the uid - pub fn gen_uid(prefix: &str) -> String { - let now = Self::gen_now(); - format!("{prefix}{now}") - } - - /// parse the string - fn parse_str(target: &str, key: &str) -> Option { - match target.find(key) { - Some(idx) => { - let idx = idx + key.len(); - let value = &target[idx..]; - match match value.split(';').nth(0) { - Some(value) => value.trim().parse(), - None => value.trim().parse(), - } { - Ok(r) => Some(r), - Err(_) => None, - } - } - None => None, - } - } - - pub async fn from_url(url: &str, with_proxy: bool) -> Result<(Self, FileData)> { - let mut builder = reqwest::ClientBuilder::new(); - - if !with_proxy { - builder = builder.no_proxy(); - } - - let resp = builder.build()?.get(url).send().await?; - let header = resp.headers(); - - // parse the Subscription Userinfo - let extra = match header.get("Subscription-Userinfo") { - Some(value) => { - let sub_info = value.to_str().unwrap_or(""); - - Some(PrfExtra { - upload: PrfItem::parse_str(sub_info, "upload=").unwrap_or(0), - download: PrfItem::parse_str(sub_info, "download=").unwrap_or(0), - total: PrfItem::parse_str(sub_info, "total=").unwrap_or(0), - expire: PrfItem::parse_str(sub_info, "expire=").unwrap_or(0), - }) - } - None => None, - }; - - let uid = PrfItem::gen_uid("r"); - let file = format!("{uid}.yaml"); - let name = uid.clone(); - let data = resp.text_with_charset("utf-8").await?; - - let item = PrfItem { - uid: Some(uid), - itype: Some("remote".into()), - name: Some(name), - desc: None, - file: Some(file), - url: Some(url.into()), - selected: None, - extra, - updated: Some(PrfItem::gen_now()), - }; - - Ok((item, data)) - } -} - -/// -/// ## Profiles Config -/// -/// Define the `profiles.yaml` schema -/// -#[derive(Default, Debug, Clone, Deserialize, Serialize)] -pub struct Profiles { - /// same as PrfConfig.current - current: Option, - - /// same as PrfConfig.chain - chain: Option>, - - /// profile list - items: Option>, -} - -impl Profiles { - pub fn new() -> Profiles { - Profiles::read_file() - } - - /// read the config from the file - pub fn read_file() -> Self { - config::read_yaml::(dirs::profiles_path()) - } - - /// 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()); - - for each in items.iter() { - if each.uid == some_uid { - self.current = some_uid; - return self.save_file(); - } - } - - bail!("invalid uid \"{uid}\""); - } - - /// append new item - /// return the new item's uid - pub fn append_item(&mut self, item: PrfItem) -> Result<()> { - if item.uid.is_none() { - bail!("the uid should not be null"); - } - - let mut items = self.items.take().unwrap_or(vec![]); - items.push(item); - self.items = Some(items); - self.save_file() - } - - /// update the item's value - pub fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> { - let mut items = self.items.take().unwrap_or(vec![]); - - macro_rules! patch { - ($lv: expr, $rv: expr, $key: tt) => { - if ($rv.$key).is_some() { - $lv.$key = $rv.$key; - } - }; - } - - 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); - - each.updated = Some(PrfItem::gen_now()); - - self.items = Some(items); - return self.save_file(); - } - } - - self.items = Some(items); - bail!("failed to found the uid \"{uid}\"") - } - - /// delete item - /// if delete the main 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) - } - - /// only generate config mapping - pub fn gen_activate(&self) -> Result { - if self.current.is_none() { - bail!("invalid main uid on profiles"); - } - - 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()); - } - - let mut new_config = Mapping::new(); - let def_config = config::read_yaml::(file_path.clone()); - - // Only the following fields are allowed: - // proxies/proxy-providers/proxy-groups/rule-providers/rules - let valid_keys = vec![ - "proxies", - "proxy-providers", - "proxy-groups", - "rule-providers", - "rules", - ]; - - valid_keys.iter().for_each(|key| { - let key = Value::String(key.to_string()); - if def_config.contains_key(&key) { - let value = def_config[&key].clone(); - new_config.insert(key, value); - } - }); - - return Ok(new_config); - } - } - - bail!("failed to found the uid \"{current}\""); - } -} diff --git a/src-tauri/src/core/profiles.rs b/src-tauri/src/core/profiles.rs index 4fdae29..0b872d7 100644 --- a/src-tauri/src/core/profiles.rs +++ b/src-tauri/src/core/profiles.rs @@ -1,27 +1,18 @@ -use super::{Clash, ClashInfo}; -use crate::utils::{config, dirs, tmpl}; -use anyhow::{bail, Result}; -use reqwest::header::HeaderMap; +use crate::utils::{config, dirs, help, tmpl}; +use anyhow::{bail, Context, Result}; use serde::{Deserialize, Serialize}; use serde_yaml::{Mapping, Value}; -use std::collections::HashMap; -use std::fs::{remove_file, File}; -use std::io::Write; -use std::path::PathBuf; -use std::time::{SystemTime, UNIX_EPOCH}; +use std::{fs, io::Write}; -/// Define the `profiles.yaml` schema -#[derive(Default, Debug, Clone, Deserialize, Serialize)] -pub struct Profiles { - /// current profile's name - pub current: Option, +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct PrfItem { + pub uid: Option, - /// profile list - pub items: Option>, -} + /// profile item type + /// enum value: remote | local | script | merge + #[serde(rename = "type")] + pub itype: Option, -#[derive(Default, Debug, Clone, Deserialize, Serialize)] -pub struct ProfileItem { /// profile name pub name: Option, @@ -32,53 +23,154 @@ pub struct ProfileItem { /// profile file pub file: Option, - /// current mode - #[serde(skip_serializing_if = "Option::is_none")] - pub mode: Option, - /// source url #[serde(skip_serializing_if = "Option::is_none")] pub url: Option, /// selected infomation #[serde(skip_serializing_if = "Option::is_none")] - pub selected: Option>, + pub selected: Option>, /// user info #[serde(skip_serializing_if = "Option::is_none")] - pub extra: Option, + pub extra: Option, /// updated time pub updated: Option, + + /// the file data + #[serde(skip)] + pub file_data: Option, } #[derive(Default, Debug, Clone, Deserialize, Serialize)] -pub struct ProfileSelected { +pub struct PrfSelected { pub name: Option, pub now: Option, } #[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)] -pub struct ProfileExtra { +pub struct PrfExtra { pub upload: usize, pub download: usize, pub total: usize, pub expire: usize, } +impl Default for PrfItem { + fn default() -> Self { + PrfItem { + uid: None, + itype: None, + name: None, + desc: None, + file: None, + url: None, + selected: None, + extra: None, + updated: None, + file_data: None, + } + } +} + +impl PrfItem { + /// ## Local type + /// create a new item from name/desc + pub fn from_local(name: String, desc: String) -> Result { + let uid = help::get_uid("l"); + let file = format!("{uid}.yaml"); + + Ok(PrfItem { + uid: Some(uid), + itype: Some("local".into()), + name: Some(name), + desc: Some(desc), + file: Some(file), + url: None, + selected: None, + extra: None, + updated: Some(help::get_now()), + file_data: Some(tmpl::ITEM_CONFIG.into()), + }) + } + + /// ## Remote type + /// create a new item from url + pub async fn from_url(url: &str, with_proxy: bool) -> Result { + let mut builder = reqwest::ClientBuilder::new(); + + if !with_proxy { + builder = builder.no_proxy(); + } + + let resp = builder.build()?.get(url).send().await?; + let header = resp.headers(); + + // parse the Subscription Userinfo + let extra = match header.get("Subscription-Userinfo") { + Some(value) => { + let sub_info = value.to_str().unwrap_or(""); + + Some(PrfExtra { + upload: help::parse_str(sub_info, "upload=").unwrap_or(0), + download: help::parse_str(sub_info, "download=").unwrap_or(0), + total: help::parse_str(sub_info, "total=").unwrap_or(0), + expire: help::parse_str(sub_info, "expire=").unwrap_or(0), + }) + } + None => None, + }; + + let uid = help::get_uid("r"); + let file = format!("{uid}.yaml"); + let name = uid.clone(); + let data = resp.text_with_charset("utf-8").await?; + + Ok(PrfItem { + uid: Some(uid), + itype: Some("remote".into()), + name: Some(name), + desc: None, + file: Some(file), + url: Some(url.into()), + selected: None, + extra, + updated: Some(help::get_now()), + file_data: Some(data), + }) + } +} + +/// +/// ## Profiles Config +/// +/// Define the `profiles.yaml` schema +/// #[derive(Default, Debug, Clone, Deserialize, Serialize)] -/// the result from url -pub struct ProfileResponse { - pub name: String, - pub file: String, - pub data: String, - pub extra: Option, +pub struct Profiles { + /// same as PrfConfig.current + current: Option, + + /// same as PrfConfig.chain + chain: Option>, + + /// profile list + items: Option>, +} + +macro_rules! patch { + ($lv: expr, $rv: expr, $key: tt) => { + if ($rv.$key).is_some() { + $lv.$key = $rv.$key; + } + }; } impl Profiles { /// read the config from the file pub fn read_file() -> Self { - config::read_yaml::(dirs::profiles_path()) + config::read_yaml::(dirs::profiles_path()) } /// save the config to the file @@ -92,303 +184,242 @@ impl Profiles { /// sync the config between file and memory pub fn sync_file(&mut self) -> Result<()> { - let data = config::read_yaml::(dirs::profiles_path()); - if data.current.is_none() { - bail!("failed to read profiles.yaml") - } else { - self.current = data.current; - self.items = data.items; - Ok(()) + let data = Self::read_file(); + if data.current.is_none() && data.items.is_none() { + bail!("failed to read profiles.yaml"); } + + self.current = data.current; + self.chain = data.chain; + self.items = data.items; + Ok(()) } - /// import the new profile from the url - /// and update the config file - pub fn import_from_url(&mut self, url: String, result: ProfileResponse) -> Result<()> { - // save the profile file - let path = dirs::app_profiles_dir().join(&result.file); - let file_data = result.data.as_bytes(); - File::create(path).unwrap().write(file_data).unwrap(); - - // update `profiles.yaml` - let data = Profiles::read_file(); - let mut items = data.items.unwrap_or(vec![]); - - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - - items.push(ProfileItem { - name: Some(result.name), - desc: Some("imported url".into()), - file: Some(result.file), - mode: Some(format!("rule")), - url: Some(url), - selected: Some(vec![]), - extra: result.extra, - updated: Some(now as usize), - }); - - self.items = Some(items); - if data.current.is_none() { - self.current = Some(0); - } - - self.save_file() + /// get the current uid + pub fn get_current(&self) -> Option { + self.current.clone() } - /// set the current and save to file - pub fn put_current(&mut self, index: usize) -> Result<()> { - let items = self.items.take().unwrap_or(vec![]); - - if index >= items.len() { - bail!("the index out of bound"); + /// 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![]); } - self.items = Some(items); - self.current = Some(index); - self.save_file() + let items = self.items.as_ref().unwrap(); + let some_uid = Some(uid.clone()); + + for each in items.iter() { + if each.uid == some_uid { + self.current = some_uid; + return self.save_file(); + } + } + + bail!("invalid uid \"{uid}\""); + } + + /// 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 item by \"{}\"", uid); } /// append new item - /// return the new item's index - pub fn append_item(&mut self, name: String, desc: String) -> Result<(usize, PathBuf)> { + /// 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's value + pub fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> { let mut items = self.items.take().unwrap_or(vec![]); - // create a new profile file - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - let file = format!("{}.yaml", now); - let path = dirs::app_profiles_dir().join(&file); + 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); - match File::create(&path).unwrap().write(tmpl::ITEM_CONFIG) { - Ok(_) => { - items.push(ProfileItem { - name: Some(name), - desc: Some(desc), - file: Some(file), - mode: None, - url: None, - selected: Some(vec![]), - extra: None, - updated: Some(now as usize), - }); + each.updated = Some(help::get_now()); - let index = items.len(); self.items = Some(items); - Ok((index, path)) - } - Err(_) => bail!("failed to create file"), - } - } - - /// update the target profile - /// and save to config file - /// only support the url item - pub fn update_item(&mut self, index: usize, result: ProfileResponse) -> Result<()> { - let mut items = self.items.take().unwrap_or(vec![]); - - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs() as usize; - - // update file - let file_path = &items[index].file.as_ref().unwrap(); - let file_path = dirs::app_profiles_dir().join(file_path); - let file_data = result.data.as_bytes(); - File::create(file_path).unwrap().write(file_data).unwrap(); - - items[index].name = Some(result.name); - items[index].extra = result.extra; - items[index].updated = Some(now); - - self.items = Some(items); - self.save_file() - } - - /// patch item - pub fn patch_item(&mut self, index: usize, profile: ProfileItem) -> Result<()> { - let mut items = self.items.take().unwrap_or(vec![]); - if index >= items.len() { - bail!("index out of range"); - } - - if profile.name.is_some() { - items[index].name = profile.name; - } - if profile.file.is_some() { - items[index].file = profile.file; - } - if profile.mode.is_some() { - items[index].mode = profile.mode; - } - if profile.url.is_some() { - items[index].url = profile.url; - } - if profile.selected.is_some() { - items[index].selected = profile.selected; - } - if profile.extra.is_some() { - items[index].extra = profile.extra; - } - - self.items = Some(items); - self.save_file() - } - - /// delete the item - pub fn delete_item(&mut self, index: usize) -> Result { - let mut current = self.current.clone().unwrap_or(0); - let mut items = self.items.clone().unwrap_or(vec![]); - - if index >= items.len() { - bail!("index out of range"); - } - - let mut rm_item = items.remove(index); - - // delete the file - if let Some(file) = rm_item.file.take() { - let file_path = dirs::app_profiles_dir().join(file); - - if file_path.exists() { - if let Err(err) = remove_file(file_path) { - log::error!("{err}"); - } + return self.save_file(); } } - let mut should_change = false; - - if current == index { - current = 0; - should_change = true; - } else if current > index { - current = current - 1; - } - - self.current = Some(current); self.items = Some(items); - - match self.save_file() { - Ok(_) => Ok(should_change), - Err(err) => Err(err), - } + bail!("failed to found the uid \"{uid}\"") } - /// activate current profile - pub fn activate(&self, clash: &Clash) -> Result<()> { - let current = self.current.unwrap_or(0); - match self.items.clone() { - Some(items) => { - if current >= items.len() { - bail!("the index out of bound"); - } + /// 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![]); + } - let profile = items[current].clone(); - let clash_config = clash.config.clone(); - let clash_info = clash.info.clone(); + // find the item + let _ = self.get_item(&uid)?; - tauri::async_runtime::spawn(async move { - let mut count = 5; // retry times - let mut err = None; - while count > 0 { - match activate_profile(&profile, &clash_config, &clash_info).await { - Ok(_) => return, - Err(e) => err = Some(e), - } - count -= 1; + self.items.as_mut().map(|items| { + let some_uid = Some(uid.clone()); + + for mut each in items.iter_mut() { + if each.uid == some_uid { + patch!(each, item, extra); + patch!(each, 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) + .unwrap() + .write(file_data.as_bytes()) + .unwrap(); } - log::error!("failed to activate for `{}`", err.unwrap()); - }); - Ok(()) - } - None => bail!("empty profiles"), - } - } -} - -/// put the profile to clash -pub async fn activate_profile( - profile_item: &ProfileItem, - clash_config: &Mapping, - clash_info: &ClashInfo, -) -> Result<()> { - // temp profile's path - let temp_path = dirs::profiles_temp_path(); - - // generate temp profile - { - let file_name = match profile_item.file.clone() { - Some(file_name) => file_name, - None => bail!("profile item should have `file` field"), - }; - - let file_path = dirs::app_profiles_dir().join(file_name); - if !file_path.exists() { - bail!( - "profile `{}` not exists", - file_path.as_os_str().to_str().unwrap() - ); - } - - // begin to generate the new profile config - let def_config = config::read_yaml::(file_path.clone()); - - // use the clash config except 5 keys below - let mut new_config = clash_config.clone(); - - // Only the following fields are allowed: - // proxies/proxy-providers/proxy-groups/rule-providers/rules - let valid_keys = vec![ - "proxies", - "proxy-providers", - "proxy-groups", - "rule-providers", - "rules", - ]; - valid_keys.iter().for_each(|key| { - let key = Value::String(key.to_string()); - if def_config.contains_key(&key) { - let value = def_config[&key].clone(); - new_config.insert(key, value); + break; + } } }); - config::save_yaml( - temp_path.clone(), - &new_config, - Some("# Clash Verge Temp File"), - )? - }; - - let server = format!("http://{}/configs", clash_info.server.clone().unwrap()); - - let mut headers = HeaderMap::new(); - headers.insert("Content-Type", "application/json".parse().unwrap()); - - if let Some(secret) = clash_info.secret.clone() { - headers.insert( - "Authorization", - format!("Bearer {}", secret).parse().unwrap(), - ); + self.save_file() } - let mut data = HashMap::new(); - data.insert("path", temp_path.as_os_str().to_str().unwrap()); + /// 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 client = reqwest::ClientBuilder::new().no_proxy().build()?; + let mut items = self.items.take().unwrap_or(vec![]); + let mut index = None; - client - .put(server) - .headers(headers) - .json(&data) - .send() - .await?; - Ok(()) + // 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) + } + + /// only generate config mapping + pub fn gen_activate(&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()); + } + + let mut new_config = Mapping::new(); + let def_config = config::read_yaml::(file_path.clone()); + + // Only the following fields are allowed: + // proxies/proxy-providers/proxy-groups/rule-providers/rules + let valid_keys = vec![ + "proxies", + "proxy-providers", + "proxy-groups", + "rule-providers", + "rules", + ]; + + valid_keys.iter().for_each(|key| { + let key = Value::String(key.to_string()); + if def_config.contains_key(&key) { + let value = def_config[&key].clone(); + new_config.insert(key, value); + } + }); + + return Ok(new_config); + } + } + + bail!("failed to found the uid \"{current}\""); + } } diff --git a/src-tauri/src/utils/help.rs b/src-tauri/src/utils/help.rs new file mode 100644 index 0000000..3ddf1bd --- /dev/null +++ b/src-tauri/src/utils/help.rs @@ -0,0 +1,86 @@ +use std::str::FromStr; +use std::time::{SystemTime, UNIX_EPOCH}; + +pub fn get_now() -> usize { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() as _ +} + +/// generate the uid +pub fn get_uid(prefix: &str) -> String { + let now = get_now(); + format!("{prefix}{now}") +} + +/// parse the string +/// xxx=123123; => 123123 +pub fn parse_str(target: &str, key: &str) -> Option { + match target.find(key) { + Some(idx) => { + let idx = idx + key.len(); + let value = &target[idx..]; + match match value.split(';').nth(0) { + Some(value) => value.trim().parse(), + None => value.trim().parse(), + } { + Ok(r) => Some(r), + Err(_) => None, + } + } + None => None, + } +} + +#[macro_export] +macro_rules! log_if_err { + ($result: expr) => { + if let Err(err) = $result { + log::error!("{err}"); + } + }; +} + +/// wrap the anyhow error +/// transform the error to String +#[macro_export] +macro_rules! wrap_err { + ($stat: expr) => { + match $stat { + Ok(a) => Ok(a), + Err(err) => { + log::error!("{}", err.to_string()); + Err(format!("{}", err.to_string())) + } + } + }; +} + +/// return the string literal error +#[macro_export] +macro_rules! ret_err { + ($str: literal) => { + return Err($str.into()) + }; +} + +#[test] +fn test_parse_value() { + let test_1 = "upload=111; download=2222; total=3333; expire=444"; + let test_2 = "attachment; filename=Clash.yaml"; + + assert_eq!(parse_str::(test_1, "upload=").unwrap(), 111); + assert_eq!(parse_str::(test_1, "download=").unwrap(), 2222); + assert_eq!(parse_str::(test_1, "total=").unwrap(), 3333); + assert_eq!(parse_str::(test_1, "expire=").unwrap(), 444); + assert_eq!( + parse_str::(test_2, "filename=").unwrap(), + format!("Clash.yaml") + ); + + assert_eq!(parse_str::(test_1, "aaa="), None); + assert_eq!(parse_str::(test_1, "upload1="), None); + assert_eq!(parse_str::(test_1, "expire1="), None); + assert_eq!(parse_str::(test_2, "attachment="), None); +} diff --git a/src-tauri/src/utils/init.rs b/src-tauri/src/utils/init.rs index 0fc16fd..facb277 100644 --- a/src-tauri/src/utils/init.rs +++ b/src-tauri/src/utils/init.rs @@ -3,7 +3,7 @@ use chrono::Local; use log::LevelFilter; use log4rs::append::console::ConsoleAppender; use log4rs::append::file::FileAppender; -use log4rs::config::{Appender, Config, Root}; +use log4rs::config::{Appender, Config, Logger, Root}; use log4rs::encode::pattern::PatternEncoder; use std::fs; use std::io::Write; @@ -28,11 +28,13 @@ fn init_log(log_dir: &PathBuf) { let config = Config::builder() .appender(Appender::builder().build("stdout", Box::new(stdout))) .appender(Appender::builder().build("file", Box::new(tofile))) - .build( - Root::builder() - .appenders(["stdout", "file"]) - .build(LevelFilter::Debug), + .logger( + Logger::builder() + .appender("file") + .additive(false) + .build("app", LevelFilter::Info), ) + .build(Root::builder().appender("stdout").build(LevelFilter::Info)) .unwrap(); log4rs::init_config(config).unwrap(); diff --git a/src-tauri/src/utils/mod.rs b/src-tauri/src/utils/mod.rs index be971d5..3db4e7c 100644 --- a/src-tauri/src/utils/mod.rs +++ b/src-tauri/src/utils/mod.rs @@ -1,6 +1,7 @@ pub mod config; pub mod dirs; -pub mod fetch; +// pub mod fetch; +pub mod help; pub mod init; pub mod resolve; pub mod server; diff --git a/src-tauri/src/utils/resolve.rs b/src-tauri/src/utils/resolve.rs index 9fe90ce..9753940 100644 --- a/src-tauri/src/utils/resolve.rs +++ b/src-tauri/src/utils/resolve.rs @@ -1,5 +1,5 @@ use super::{init, server}; -use crate::{core::Profiles, states}; +use crate::{core::Profiles, log_if_err, states}; use tauri::{App, AppHandle, Manager}; /// handle something when start app @@ -21,14 +21,10 @@ pub fn resolve_setup(app: &App) { let mut verge = verge_state.0.lock().unwrap(); let mut profiles = profiles_state.0.lock().unwrap(); - if let Err(err) = clash.run_sidecar() { - log::error!("{err}"); - } + log_if_err!(clash.run_sidecar()); *profiles = Profiles::read_file(); - if let Err(err) = profiles.activate(&clash) { - log::error!("{err}"); - } + log_if_err!(clash.activate(&profiles)); verge.init_sysproxy(clash.info.port.clone()); // enable tun mode @@ -41,9 +37,7 @@ pub fn resolve_setup(app: &App) { } verge.init_launch(); - if let Err(err) = verge.sync_launch() { - log::error!("{err}"); - } + log_if_err!(verge.sync_launch()); } /// reset system proxy diff --git a/src-tauri/src/utils/tmpl.rs b/src-tauri/src/utils/tmpl.rs index 2b9c04a..2462c2d 100644 --- a/src-tauri/src/utils/tmpl.rs +++ b/src-tauri/src/utils/tmpl.rs @@ -14,7 +14,7 @@ secret: "" /// template for `profiles.yaml` pub const PROFILES_CONFIG: &[u8] = b"# Profiles Config for Clash Verge -current: 0 +current: ~ items: ~ "; @@ -32,7 +32,7 @@ system_proxy_bypass: localhost;127.*;10.*;192.168.*; "; /// template for new a profile item -pub const ITEM_CONFIG: &[u8] = b"# Profile Template for clash verge\n\n +pub const ITEM_CONFIG: &str = "# Profile Template for clash verge\n\n # proxies defination (optional, the same as clash) proxies:\n # proxy-groups (optional, the same as clash) diff --git a/src/components/profile/profile-item.tsx b/src/components/profile/profile-item.tsx index e42acc4..b39459d 100644 --- a/src/components/profile/profile-item.tsx +++ b/src/components/profile/profile-item.tsx @@ -39,14 +39,14 @@ const round = keyframes` `; interface Props { - index: number; + // index: number; selected: boolean; itemData: CmdType.ProfileItem; onSelect: (force: boolean) => void; } const ProfileItem: React.FC = (props) => { - const { index, selected, itemData, onSelect } = props; + const { selected, itemData, onSelect } = props; const { mutate } = useSWRConfig(); const [loading, setLoading] = useState(false); @@ -69,7 +69,7 @@ const ProfileItem: React.FC = (props) => { const onView = async () => { setAnchorEl(null); try { - await viewProfile(index); + await viewProfile(itemData.uid); } catch (err: any) { Notice.error(err.toString()); } @@ -85,7 +85,7 @@ const ProfileItem: React.FC = (props) => { if (loading) return; setLoading(true); try { - await updateProfile(index, withProxy); + await updateProfile(itemData.uid, withProxy); mutate("getProfiles"); } catch (err: any) { Notice.error(err.toString()); @@ -98,7 +98,7 @@ const ProfileItem: React.FC = (props) => { setAnchorEl(null); try { - await deleteProfile(index); + await deleteProfile(itemData.uid); mutate("getProfiles"); } catch (err: any) { Notice.error(err.toString()); diff --git a/src/components/proxy/proxy-group.tsx b/src/components/proxy/proxy-group.tsx index 2a3d61d..acd516d 100644 --- a/src/components/proxy/proxy-group.tsx +++ b/src/components/proxy/proxy-group.tsx @@ -1,5 +1,5 @@ +import useSWR, { useSWRConfig } from "swr"; import { useEffect, useRef, useState } from "react"; -import { useSWRConfig } from "swr"; import { useLockFn } from "ahooks"; import { Virtuoso } from "react-virtuoso"; import { @@ -46,6 +46,8 @@ const ProxyGroup = ({ group }: Props) => { const virtuosoRef = useRef(); const filterProxies = useFilterProxy(proxies, group.name, filterText); + const { data: profiles } = useSWR("getProfiles", getProfiles); + const onChangeProxy = useLockFn(async (name: string) => { // Todo: support another proxy group type if (group.type !== "Selector") return; @@ -60,8 +62,7 @@ const ProxyGroup = ({ group }: Props) => { } try { - const profiles = await getProfiles(); - const profile = profiles.items![profiles.current!]!; + const profile = profiles?.items?.find((p) => p.uid === profiles.current); if (!profile) return; if (!profile.selected) profile.selected = []; @@ -74,7 +75,7 @@ const ProxyGroup = ({ group }: Props) => { } else { profile.selected[index] = { name: group.name, now: name }; } - await patchProfile(profiles.current!, profile); + await patchProfile(profiles!.current!, profile); } catch (err) { console.error(err); } diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx index 802e34c..b6491ab 100644 --- a/src/pages/profiles.tsx +++ b/src/pages/profiles.tsx @@ -10,7 +10,6 @@ import { newProfile, } from "../services/cmds"; import { getProxies, updateProxy } from "../services/api"; -import noop from "../utils/noop"; import Notice from "../components/base/base-notice"; import BasePage from "../components/base/base-page"; import ProfileItem from "../components/profile/profile-item"; @@ -28,7 +27,7 @@ const ProfilePage = () => { if (!profiles.items) profiles.items = []; const current = profiles.current; - const profile = profiles.items![current]; + const profile = profiles.items.find((p) => p.uid === current); if (!profile) return; setTimeout(async () => { @@ -72,9 +71,17 @@ const ProfilePage = () => { try { await importProfile(url); - mutate("getProfiles", getProfiles()); - if (!profiles.items?.length) selectProfile(0).catch(noop); Notice.success("Successfully import profile."); + + getProfiles().then((newProfiles) => { + mutate("getProfiles", newProfiles); + + if (!newProfiles.current && newProfiles.items?.length) { + const current = newProfiles.items[0].uid; + selectProfile(current); + mutate("getProfiles", { ...newProfiles, current }, true); + } + }); } catch { Notice.error("Failed to import profile."); } finally { @@ -82,12 +89,12 @@ const ProfilePage = () => { } }; - const onSelect = useLockFn(async (index: number, force: boolean) => { - if (!force && index === profiles.current) return; + const onSelect = useLockFn(async (current: string, force: boolean) => { + if (!force && current === profiles.current) return; try { - await selectProfile(index); - mutate("getProfiles", { ...profiles, current: index }, true); + await selectProfile(current); + mutate("getProfiles", { ...profiles, current: current }, true); } catch (err: any) { err && Notice.error(err.toString()); } @@ -131,13 +138,12 @@ const ProfilePage = () => { - {profiles?.items?.map((item, idx) => ( + {profiles?.items?.map((item) => ( onSelect(idx, f)} + onSelect={(f) => onSelect(item.uid, f)} /> ))} diff --git a/src/services/cmds.ts b/src/services/cmds.ts index b8d9d4a..595a4db 100644 --- a/src/services/cmds.ts +++ b/src/services/cmds.ts @@ -13,7 +13,7 @@ export async function newProfile(name: string, desc: string) { return invoke("new_profile", { name, desc }); } -export async function viewProfile(index: number) { +export async function viewProfile(index: string) { return invoke("view_profile", { index }); } @@ -21,22 +21,22 @@ export async function importProfile(url: string) { return invoke("import_profile", { url, withProxy: true }); } -export async function updateProfile(index: number, withProxy: boolean) { +export async function updateProfile(index: string, withProxy: boolean) { return invoke("update_profile", { index, withProxy }); } -export async function deleteProfile(index: number) { +export async function deleteProfile(index: string) { return invoke("delete_profile", { index }); } export async function patchProfile( - index: number, + index: string, profile: CmdType.ProfileItem ) { return invoke("patch_profile", { index, profile }); } -export async function selectProfile(index: number) { +export async function selectProfile(index: string) { return invoke("select_profile", { index }); } diff --git a/src/services/types.ts b/src/services/types.ts index 8843766..07ace0e 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -86,6 +86,8 @@ export namespace CmdType { } export interface ProfileItem { + uid: string; + type?: string; name?: string; desc?: string; file?: string; @@ -105,7 +107,8 @@ export namespace CmdType { } export interface ProfilesConfig { - current?: number; + current?: string; + chain?: string[]; items?: ProfileItem[]; }