Files
clash-proxy/src-tauri/src/core/profiles.rs

440 lines
11 KiB
Rust
Raw Normal View History

2022-03-01 08:58:47 +08:00
use crate::utils::{config, dirs, help, tmpl};
use anyhow::{bail, Context, Result};
2021-12-13 02:29:02 +08:00
use serde::{Deserialize, Serialize};
2022-01-07 23:29:20 +08:00
use serde_yaml::{Mapping, Value};
2022-03-01 08:58:47 +08:00
use std::{fs, io::Write};
2021-12-13 02:29:02 +08:00
2022-03-01 08:58:47 +08:00
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PrfItem {
pub uid: Option<String>,
2021-12-13 02:29:02 +08:00
2022-03-01 08:58:47 +08:00
/// profile item type
/// enum value: remote | local | script | merge
#[serde(rename = "type")]
pub itype: Option<String>,
2021-12-13 02:29:02 +08:00
/// profile name
pub name: Option<String>,
2022-02-28 01:34:25 +08:00
2022-02-07 17:26:05 +08:00
/// profile description
2022-02-28 01:34:25 +08:00
#[serde(skip_serializing_if = "Option::is_none")]
2022-02-07 17:26:05 +08:00
pub desc: Option<String>,
2022-02-28 01:34:25 +08:00
2021-12-13 02:29:02 +08:00
/// profile file
pub file: Option<String>,
2022-02-28 01:34:25 +08:00
2021-12-13 02:29:02 +08:00
/// source url
2022-02-28 01:34:25 +08:00
#[serde(skip_serializing_if = "Option::is_none")]
2021-12-13 02:29:02 +08:00
pub url: Option<String>,
2022-02-28 01:34:25 +08:00
2021-12-13 02:29:02 +08:00
/// selected infomation
2022-02-28 01:34:25 +08:00
#[serde(skip_serializing_if = "Option::is_none")]
2022-03-01 08:58:47 +08:00
pub selected: Option<Vec<PrfSelected>>,
2022-02-28 01:34:25 +08:00
2021-12-13 02:29:02 +08:00
/// user info
2022-02-28 01:34:25 +08:00
#[serde(skip_serializing_if = "Option::is_none")]
2022-03-01 08:58:47 +08:00
pub extra: Option<PrfExtra>,
2022-02-28 01:34:25 +08:00
/// updated time
pub updated: Option<usize>,
2022-03-01 08:58:47 +08:00
/// the file data
#[serde(skip)]
pub file_data: Option<String>,
2021-12-13 02:29:02 +08:00
}
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
2022-03-01 08:58:47 +08:00
pub struct PrfSelected {
2021-12-13 02:29:02 +08:00
pub name: Option<String>,
pub now: Option<String>,
}
#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)]
2022-03-01 08:58:47 +08:00
pub struct PrfExtra {
2021-12-18 02:29:54 +08:00
pub upload: usize,
pub download: usize,
pub total: usize,
pub expire: usize,
2021-12-13 02:29:02 +08:00
}
2021-12-14 22:33:42 +08:00
2022-03-01 08:58:47 +08:00
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<PrfItem> {
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<PrfItem> {
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
///
2021-12-14 22:33:42 +08:00
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
2022-03-01 08:58:47 +08:00
pub struct Profiles {
/// same as PrfConfig.current
current: Option<String>,
/// same as PrfConfig.chain
chain: Option<Vec<String>>,
/// profile list
items: Option<Vec<PrfItem>>,
}
macro_rules! patch {
($lv: expr, $rv: expr, $key: tt) => {
if ($rv.$key).is_some() {
$lv.$key = $rv.$key;
}
};
2021-12-14 22:33:42 +08:00
}
2022-01-05 02:00:59 +08:00
impl Profiles {
2022-01-05 02:00:59 +08:00
/// read the config from the file
pub fn read_file() -> Self {
2022-03-01 11:05:33 +08:00
let mut profiles = config::read_yaml::<Self>(dirs::profiles_path());
if profiles.items.is_none() {
profiles.items = Some(vec![]);
}
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
2022-01-05 02:00:59 +08:00
}
/// save the config to the file
2022-02-28 01:34:25 +08:00
pub fn save_file(&self) -> Result<()> {
2022-01-05 02:00:59 +08:00
config::save_yaml(
2022-02-28 01:34:25 +08:00
dirs::profiles_path(),
2022-01-05 02:00:59 +08:00
self,
Some("# Profiles Config for Clash Verge\n\n"),
)
}
/// sync the config between file and memory
2022-02-28 01:34:25 +08:00
pub fn sync_file(&mut self) -> Result<()> {
2022-03-01 08:58:47 +08:00
let data = Self::read_file();
if data.current.is_none() && data.items.is_none() {
bail!("failed to read profiles.yaml");
2022-01-05 02:00:59 +08:00
}
2022-03-01 08:58:47 +08:00
self.current = data.current;
self.chain = data.chain;
self.items = data.items;
Ok(())
2022-01-05 02:00:59 +08:00
}
2022-03-01 08:58:47 +08:00
/// get the current uid
pub fn get_current(&self) -> Option<String> {
self.current.clone()
}
2022-01-05 02:00:59 +08:00
2022-03-01 08:58:47 +08:00
/// 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![]);
2022-01-05 02:00:59 +08:00
}
2022-03-01 08:58:47 +08:00
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}\"");
2022-01-05 02:00:59 +08:00
}
2022-03-01 08:58:47 +08:00
/// 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());
2022-01-05 02:00:59 +08:00
2022-03-01 08:58:47 +08:00
for each in items.iter() {
if each.uid == some_uid {
return Ok(each);
}
}
2022-01-05 02:00:59 +08:00
}
2022-03-01 08:58:47 +08:00
bail!("failed to get the item by \"{}\"", uid);
2022-01-05 02:00:59 +08:00
}
2022-02-07 17:26:05 +08:00
/// append new item
2022-03-01 08:58:47 +08:00
/// 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");
}
2022-02-07 17:26:05 +08:00
2022-03-01 08:58:47 +08:00
// 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");
2022-02-07 17:26:05 +08:00
}
2022-03-01 08:58:47 +08:00
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![]);
2022-02-07 17:26:05 +08:00
}
2022-03-01 08:58:47 +08:00
self.items.as_mut().map(|items| items.push(item));
self.save_file()
2022-02-07 17:26:05 +08:00
}
2022-03-01 08:58:47 +08:00
/// update the item's value
pub fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> {
2022-01-05 02:00:59 +08:00
let mut items = self.items.take().unwrap_or(vec![]);
2022-03-01 08:58:47 +08:00
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);
2022-01-05 02:00:59 +08:00
2022-03-01 08:58:47 +08:00
each.updated = Some(help::get_now());
2022-01-05 02:00:59 +08:00
2022-03-01 08:58:47 +08:00
self.items = Some(items);
return self.save_file();
}
}
2022-01-05 02:00:59 +08:00
self.items = Some(items);
2022-03-01 08:58:47 +08:00
bail!("failed to found the uid \"{uid}\"")
2022-01-05 02:00:59 +08:00
}
2022-03-01 08:58:47 +08:00
/// 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![]);
2022-01-05 02:00:59 +08:00
}
2022-03-01 08:58:47 +08:00
// find the item
let _ = self.get_item(&uid)?;
2022-01-05 02:00:59 +08:00
2022-03-01 08:58:47 +08:00
self.items.as_mut().map(|items| {
let some_uid = Some(uid.clone());
2022-01-05 02:00:59 +08:00
2022-03-01 08:58:47 +08:00
for mut each in items.iter_mut() {
if each.uid == some_uid {
patch!(each, item, extra);
patch!(each, item, updated);
2022-01-05 02:00:59 +08:00
2022-03-01 08:58:47 +08:00
// 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)));
2022-01-05 02:00:59 +08:00
2022-03-01 08:58:47 +08:00
// the file must exists
each.file = Some(file.clone());
2022-02-17 01:58:12 +08:00
2022-03-01 08:58:47 +08:00
let path = dirs::app_profiles_dir().join(&file);
2022-02-17 01:58:12 +08:00
2022-03-01 08:58:47 +08:00
fs::File::create(path)
.unwrap()
.write(file_data.as_bytes())
.unwrap();
}
break;
2022-02-17 01:58:12 +08:00
}
}
2022-03-01 08:58:47 +08:00
});
2022-01-05 02:00:59 +08:00
2022-03-01 08:58:47 +08:00
self.save_file()
}
2022-01-08 14:21:12 +08:00
2022-03-01 08:58:47 +08:00
/// delete item
/// if delete the current then return true
pub fn delete_item(&mut self, uid: String) -> Result<bool> {
let current = self.current.as_ref().unwrap_or(&uid);
let current = current.clone();
2022-01-08 14:21:12 +08:00
2022-03-01 08:58:47 +08:00
let mut items = self.items.take().unwrap_or(vec![]);
let mut index = None;
2022-01-08 14:21:12 +08:00
2022-03-01 08:58:47 +08:00
// get the index
for i in 0..items.len() {
if items[i].uid == Some(uid.clone()) {
index = Some(i);
break;
}
2022-01-08 14:21:12 +08:00
}
2022-01-07 23:29:20 +08:00
2022-03-01 08:58:47 +08:00
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);
2022-01-07 23:29:20 +08:00
}
2022-03-01 08:58:47 +08:00
});
}
2022-01-07 23:29:20 +08:00
2022-03-01 08:58:47 +08:00
// delete the original uid
if current == uid {
self.current = match items.len() > 0 {
true => items[0].uid.clone(),
false => None,
};
2022-01-07 23:29:20 +08:00
}
2022-03-01 08:58:47 +08:00
self.items = Some(items);
self.save_file()?;
Ok(current == uid)
2022-01-07 23:29:20 +08:00
}
2022-03-01 08:58:47 +08:00
/// only generate config mapping
pub fn gen_activate(&self) -> Result<Mapping> {
let config = Mapping::new();
2022-01-07 23:29:20 +08:00
2022-03-01 08:58:47 +08:00
if self.current.is_none() || self.items.is_none() {
return Ok(config);
2022-01-07 23:29:20 +08:00
}
2022-03-01 08:58:47 +08:00
let current = self.current.clone().unwrap();
2022-01-21 02:31:44 +08:00
2022-03-01 08:58:47 +08:00
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"),
};
2022-01-07 23:29:20 +08:00
2022-03-01 08:58:47 +08:00
if !file_path.exists() {
bail!("failed to read the file \"{}\"", file_path.display());
}
2022-01-07 23:29:20 +08:00
2022-03-01 08:58:47 +08:00
let mut new_config = Mapping::new();
let def_config = config::read_yaml::<Mapping>(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);
}
});
2022-01-07 23:29:20 +08:00
2022-03-01 08:58:47 +08:00
return Ok(new_config);
}
}
2022-02-28 01:34:25 +08:00
2022-03-01 08:58:47 +08:00
bail!("failed to found the uid \"{current}\"");
}
2022-01-05 02:00:59 +08:00
}