profiles.rs 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. use super::{Clash, ClashInfo};
  2. use crate::utils::{config, dirs};
  3. use reqwest::header::HeaderMap;
  4. use serde::{Deserialize, Serialize};
  5. use serde_yaml::{Mapping, Value};
  6. use std::collections::HashMap;
  7. use std::env::temp_dir;
  8. use std::fs::File;
  9. use std::io::Write;
  10. use std::time::{SystemTime, UNIX_EPOCH};
  11. /// Define the `profiles.yaml` schema
  12. #[derive(Default, Debug, Clone, Deserialize, Serialize)]
  13. pub struct Profiles {
  14. /// current profile's name
  15. pub current: Option<usize>,
  16. /// profile list
  17. pub items: Option<Vec<ProfileItem>>,
  18. }
  19. #[derive(Default, Debug, Clone, Deserialize, Serialize)]
  20. pub struct ProfileItem {
  21. /// profile name
  22. pub name: Option<String>,
  23. /// profile file
  24. pub file: Option<String>,
  25. /// current mode
  26. pub mode: Option<String>,
  27. /// source url
  28. pub url: Option<String>,
  29. /// selected infomation
  30. pub selected: Option<Vec<ProfileSelected>>,
  31. /// user info
  32. pub extra: Option<ProfileExtra>,
  33. /// updated time
  34. pub updated: Option<usize>,
  35. }
  36. #[derive(Default, Debug, Clone, Deserialize, Serialize)]
  37. pub struct ProfileSelected {
  38. pub name: Option<String>,
  39. pub now: Option<String>,
  40. }
  41. #[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)]
  42. pub struct ProfileExtra {
  43. pub upload: usize,
  44. pub download: usize,
  45. pub total: usize,
  46. pub expire: usize,
  47. }
  48. #[derive(Default, Debug, Clone, Deserialize, Serialize)]
  49. /// the result from url
  50. pub struct ProfileResponse {
  51. pub name: String,
  52. pub file: String,
  53. pub data: String,
  54. pub extra: ProfileExtra,
  55. }
  56. static PROFILE_YAML: &str = "profiles.yaml";
  57. static PROFILE_TEMP: &str = "clash-verge-runtime.yaml";
  58. impl Profiles {
  59. /// read the config from the file
  60. pub fn read_file() -> Self {
  61. config::read_yaml::<Profiles>(dirs::app_home_dir().join(PROFILE_YAML))
  62. }
  63. /// save the config to the file
  64. pub fn save_file(&self) -> Result<(), String> {
  65. config::save_yaml(
  66. dirs::app_home_dir().join(PROFILE_YAML),
  67. self,
  68. Some("# Profiles Config for Clash Verge\n\n"),
  69. )
  70. }
  71. /// sync the config between file and memory
  72. pub fn sync_file(&mut self) -> Result<(), String> {
  73. let data = config::read_yaml::<Self>(dirs::app_home_dir().join(PROFILE_YAML));
  74. if data.current.is_none() {
  75. Err("failed to read profiles.yaml".into())
  76. } else {
  77. self.current = data.current;
  78. self.items = data.items;
  79. Ok(())
  80. }
  81. }
  82. /// import the new profile from the url
  83. /// and update the config file
  84. pub fn import_from_url(&mut self, url: String, result: ProfileResponse) -> Result<(), String> {
  85. // save the profile file
  86. let path = dirs::app_home_dir().join("profiles").join(&result.file);
  87. let file_data = result.data.as_bytes();
  88. File::create(path).unwrap().write(file_data).unwrap();
  89. // update `profiles.yaml`
  90. let data = Profiles::read_file();
  91. let mut items = data.items.unwrap_or(vec![]);
  92. let now = SystemTime::now()
  93. .duration_since(UNIX_EPOCH)
  94. .unwrap()
  95. .as_secs();
  96. items.push(ProfileItem {
  97. name: Some(result.name),
  98. file: Some(result.file),
  99. mode: Some(format!("rule")),
  100. url: Some(url),
  101. selected: Some(vec![]),
  102. extra: Some(result.extra),
  103. updated: Some(now as usize),
  104. });
  105. self.items = Some(items);
  106. if data.current.is_none() {
  107. self.current = Some(0);
  108. }
  109. self.save_file()
  110. }
  111. /// set the current and save to file
  112. pub fn put_current(&mut self, index: usize) -> Result<(), String> {
  113. let items = self.items.take().unwrap_or(vec![]);
  114. if index >= items.len() {
  115. return Err("the index out of bound".into());
  116. }
  117. self.items = Some(items);
  118. self.current = Some(index);
  119. self.save_file()
  120. }
  121. /// update the target profile
  122. /// and save to config file
  123. /// only support the url item
  124. pub fn update_item(&mut self, index: usize, result: ProfileResponse) -> Result<(), String> {
  125. let mut items = self.items.take().unwrap_or(vec![]);
  126. let now = SystemTime::now()
  127. .duration_since(UNIX_EPOCH)
  128. .unwrap()
  129. .as_secs() as usize;
  130. // update file
  131. let file_path = &items[index].file.as_ref().unwrap();
  132. let file_path = dirs::app_home_dir().join("profiles").join(file_path);
  133. let file_data = result.data.as_bytes();
  134. File::create(file_path).unwrap().write(file_data).unwrap();
  135. items[index].name = Some(result.name);
  136. items[index].extra = Some(result.extra);
  137. items[index].updated = Some(now);
  138. self.items = Some(items);
  139. self.save_file()
  140. }
  141. /// patch item
  142. pub fn patch_item(&mut self, index: usize, profile: ProfileItem) -> Result<(), String> {
  143. let mut items = self.items.take().unwrap_or(vec![]);
  144. if index >= items.len() {
  145. return Err("index out of bound".into());
  146. }
  147. if profile.name.is_some() {
  148. items[index].name = profile.name;
  149. }
  150. if profile.file.is_some() {
  151. items[index].file = profile.file;
  152. }
  153. if profile.mode.is_some() {
  154. items[index].mode = profile.mode;
  155. }
  156. if profile.url.is_some() {
  157. items[index].url = profile.url;
  158. }
  159. if profile.selected.is_some() {
  160. items[index].selected = profile.selected;
  161. }
  162. if profile.extra.is_some() {
  163. items[index].extra = profile.extra;
  164. }
  165. self.items = Some(items);
  166. self.save_file()
  167. }
  168. /// delete the item
  169. pub fn delete_item(&mut self, index: usize) -> Result<bool, String> {
  170. let mut current = self.current.clone().unwrap_or(0);
  171. let mut items = self.items.clone().unwrap_or(vec![]);
  172. if index >= items.len() {
  173. return Err("index out of bound".into());
  174. }
  175. items.remove(index);
  176. let mut should_change = false;
  177. if current == index {
  178. current = 0;
  179. should_change = true;
  180. } else if current > index {
  181. current = current - 1;
  182. }
  183. self.current = Some(current);
  184. self.items = Some(items);
  185. match self.save_file() {
  186. Ok(_) => Ok(should_change),
  187. Err(err) => Err(err),
  188. }
  189. }
  190. /// activate current profile
  191. pub fn activate(&self, clash: &Clash) -> Result<(), String> {
  192. let current = self.current.unwrap_or(0);
  193. match self.items.clone() {
  194. Some(items) => {
  195. if current >= items.len() {
  196. return Err("the index out of bound".into());
  197. }
  198. let profile = items[current].clone();
  199. let clash_config = clash.config.clone();
  200. let clash_info = clash.info.clone();
  201. tauri::async_runtime::spawn(async move {
  202. let mut count = 5; // retry times
  203. let mut err = String::from("");
  204. while count > 0 {
  205. match activate_profile(&profile, &clash_config, &clash_info).await {
  206. Ok(_) => return,
  207. Err(e) => err = e,
  208. }
  209. count -= 1;
  210. }
  211. log::error!("failed to activate for `{}`", err);
  212. });
  213. Ok(())
  214. }
  215. None => Err("empty profiles".into()),
  216. }
  217. }
  218. }
  219. /// put the profile to clash
  220. pub async fn activate_profile(
  221. profile_item: &ProfileItem,
  222. clash_config: &Mapping,
  223. clash_info: &ClashInfo,
  224. ) -> Result<(), String> {
  225. // temp profile's path
  226. let temp_path = temp_dir().join(PROFILE_TEMP);
  227. // generate temp profile
  228. {
  229. let file_name = match profile_item.file.clone() {
  230. Some(file_name) => file_name,
  231. None => return Err("profile item should have `file` field".into()),
  232. };
  233. let file_path = dirs::app_home_dir().join("profiles").join(file_name);
  234. if !file_path.exists() {
  235. return Err(format!(
  236. "profile `{}` not exists",
  237. file_path.as_os_str().to_str().unwrap()
  238. ));
  239. }
  240. // begin to generate the new profile config
  241. let def_config = config::read_yaml::<Mapping>(file_path.clone());
  242. let mut new_config = Mapping::new();
  243. // Only the following fields are allowed:
  244. // proxies/proxy-providers/proxy-groups/rule-providers/rules
  245. let valid_keys = vec![
  246. "proxies",
  247. "proxy-providers",
  248. "proxy-groups",
  249. "rule-providers",
  250. "rules",
  251. ];
  252. valid_keys.iter().for_each(|key| {
  253. let key = Value::String(key.to_string());
  254. if def_config.contains_key(&key) {
  255. let value = def_config[&key].clone();
  256. new_config.insert(key, value);
  257. }
  258. });
  259. // add some of the clash `config.yaml` config to it
  260. let valid_keys = vec![
  261. "mixed-port",
  262. "log-level",
  263. "allow-lan",
  264. "external-controller",
  265. "secret",
  266. "ipv6",
  267. ];
  268. valid_keys.iter().for_each(|key| {
  269. let key = Value::String(key.to_string());
  270. if clash_config.contains_key(&key) {
  271. let value = clash_config[&key].clone();
  272. new_config.insert(key, value);
  273. }
  274. });
  275. config::save_yaml(
  276. temp_path.clone(),
  277. &new_config,
  278. Some("# Clash Verge Temp File"),
  279. )?
  280. };
  281. let server = format!("http://{}/configs", clash_info.server.clone().unwrap());
  282. let mut headers = HeaderMap::new();
  283. headers.insert("Content-Type", "application/json".parse().unwrap());
  284. if let Some(secret) = clash_info.secret.clone() {
  285. headers.insert(
  286. "Authorization",
  287. format!("Bearer {}", secret).parse().unwrap(),
  288. );
  289. }
  290. let mut data = HashMap::new();
  291. data.insert("path", temp_path.as_os_str().to_str().unwrap());
  292. let client = match reqwest::ClientBuilder::new().no_proxy().build() {
  293. Ok(c) => c,
  294. Err(_) => return Err("failed to create http::put".into()),
  295. };
  296. match client.put(server).headers(headers).json(&data).send().await {
  297. Ok(_) => Ok(()),
  298. Err(err) => Err(format!("request failed `{}`", err.to_string())),
  299. }
  300. }