123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349 |
- use super::{Clash, ClashInfo};
- use crate::utils::{config, dirs};
- use reqwest::header::HeaderMap;
- use serde::{Deserialize, Serialize};
- use serde_yaml::{Mapping, Value};
- use std::collections::HashMap;
- use std::env::temp_dir;
- use std::fs::File;
- use std::io::Write;
- use std::time::{SystemTime, UNIX_EPOCH};
- /// Define the `profiles.yaml` schema
- #[derive(Default, Debug, Clone, Deserialize, Serialize)]
- pub struct Profiles {
- /// current profile's name
- pub current: Option<usize>,
- /// profile list
- pub items: Option<Vec<ProfileItem>>,
- }
- #[derive(Default, Debug, Clone, Deserialize, Serialize)]
- pub struct ProfileItem {
- /// profile name
- pub name: Option<String>,
- /// profile file
- pub file: Option<String>,
- /// current mode
- pub mode: Option<String>,
- /// source url
- pub url: Option<String>,
- /// selected infomation
- pub selected: Option<Vec<ProfileSelected>>,
- /// user info
- pub extra: Option<ProfileExtra>,
- /// updated time
- pub updated: Option<usize>,
- }
- #[derive(Default, Debug, Clone, Deserialize, Serialize)]
- pub struct ProfileSelected {
- pub name: Option<String>,
- pub now: Option<String>,
- }
- #[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)]
- pub struct ProfileExtra {
- pub upload: usize,
- pub download: usize,
- pub total: usize,
- pub expire: usize,
- }
- #[derive(Default, Debug, Clone, Deserialize, Serialize)]
- /// the result from url
- pub struct ProfileResponse {
- pub name: String,
- pub file: String,
- pub data: String,
- pub extra: ProfileExtra,
- }
- static PROFILE_YAML: &str = "profiles.yaml";
- static PROFILE_TEMP: &str = "clash-verge-runtime.yaml";
- impl Profiles {
- /// read the config from the file
- pub fn read_file() -> Self {
- config::read_yaml::<Profiles>(dirs::app_home_dir().join(PROFILE_YAML))
- }
- /// save the config to the file
- pub fn save_file(&self) -> Result<(), String> {
- config::save_yaml(
- dirs::app_home_dir().join(PROFILE_YAML),
- self,
- Some("# Profiles Config for Clash Verge\n\n"),
- )
- }
- /// sync the config between file and memory
- pub fn sync_file(&mut self) -> Result<(), String> {
- let data = config::read_yaml::<Self>(dirs::app_home_dir().join(PROFILE_YAML));
- if data.current.is_none() {
- Err("failed to read profiles.yaml".into())
- } else {
- self.current = data.current;
- 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<(), String> {
- // save the profile file
- let path = dirs::app_home_dir().join("profiles").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),
- file: Some(result.file),
- mode: Some(format!("rule")),
- url: Some(url),
- selected: Some(vec![]),
- extra: Some(result.extra),
- updated: Some(now as usize),
- });
- self.items = Some(items);
- if data.current.is_none() {
- self.current = Some(0);
- }
- self.save_file()
- }
- /// set the current and save to file
- pub fn put_current(&mut self, index: usize) -> Result<(), String> {
- let items = self.items.take().unwrap_or(vec![]);
- if index >= items.len() {
- return Err("the index out of bound".into());
- }
- self.items = Some(items);
- self.current = Some(index);
- self.save_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<(), String> {
- 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_home_dir().join("profiles").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 = Some(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<(), String> {
- let mut items = self.items.take().unwrap_or(vec![]);
- if index >= items.len() {
- return Err("index out of bound".into());
- }
- 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<bool, String> {
- let mut current = self.current.clone().unwrap_or(0);
- let mut items = self.items.clone().unwrap_or(vec![]);
- if index >= items.len() {
- return Err("index out of bound".into());
- }
- items.remove(index);
- 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),
- }
- }
- /// activate current profile
- pub fn activate(&self, clash: &Clash) -> Result<(), String> {
- let current = self.current.unwrap_or(0);
- match self.items.clone() {
- Some(items) => {
- if current >= items.len() {
- return Err("the index out of bound".into());
- }
- let profile = items[current].clone();
- let clash_config = clash.config.clone();
- let clash_info = clash.info.clone();
- tauri::async_runtime::spawn(async move {
- let mut count = 5; // retry times
- let mut err = String::from("");
- while count > 0 {
- match activate_profile(&profile, &clash_config, &clash_info).await {
- Ok(_) => return,
- Err(e) => err = e,
- }
- count -= 1;
- }
- log::error!("failed to activate for `{}`", err);
- });
- Ok(())
- }
- None => Err("empty profiles".into()),
- }
- }
- }
- /// put the profile to clash
- pub async fn activate_profile(
- profile_item: &ProfileItem,
- clash_config: &Mapping,
- clash_info: &ClashInfo,
- ) -> Result<(), String> {
- // temp profile's path
- let temp_path = temp_dir().join(PROFILE_TEMP);
- // generate temp profile
- {
- let file_name = match profile_item.file.clone() {
- Some(file_name) => file_name,
- None => return Err("profile item should have `file` field".into()),
- };
- let file_path = dirs::app_home_dir().join("profiles").join(file_name);
- if !file_path.exists() {
- return Err(format!(
- "profile `{}` not exists",
- file_path.as_os_str().to_str().unwrap()
- ));
- }
- // begin to generate the new profile config
- let def_config = config::read_yaml::<Mapping>(file_path.clone());
- let mut new_config = Mapping::new();
- // 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);
- }
- });
- // add some of the clash `config.yaml` config to it
- let valid_keys = vec![
- "mixed-port",
- "log-level",
- "allow-lan",
- "external-controller",
- "secret",
- "ipv6",
- ];
- valid_keys.iter().for_each(|key| {
- let key = Value::String(key.to_string());
- if clash_config.contains_key(&key) {
- let value = clash_config[&key].clone();
- new_config.insert(key, value);
- }
- });
- 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(),
- );
- }
- let mut data = HashMap::new();
- data.insert("path", temp_path.as_os_str().to_str().unwrap());
- let client = match reqwest::ClientBuilder::new().no_proxy().build() {
- Ok(c) => c,
- Err(_) => return Err("failed to create http::put".into()),
- };
- match client.put(server).headers(headers).json(&data).send().await {
- Ok(_) => Ok(()),
- Err(err) => Err(format!("request failed `{}`", err.to_string())),
- }
- }
|