Files
clash-verge-rev/src-tauri/src/core/profiles.rs
T

394 lines
10 KiB
Rust
Raw Normal View History

2022-01-21 02:31:44 +08:00
use super::{Clash, ClashInfo};
2022-02-18 23:57:13 +08:00
use crate::utils::{config, dirs, tmpl};
2022-02-28 01:34:25 +08:00
use anyhow::{bail, Result};
2022-01-07 23:29:20 +08:00
use reqwest::header::HeaderMap;
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};
use std::collections::HashMap;
2022-02-17 01:58:12 +08:00
use std::fs::{remove_file, File};
2022-01-05 02:00:59 +08:00
use std::io::Write;
2022-02-07 17:26:05 +08:00
use std::path::PathBuf;
2022-01-05 02:00:59 +08:00
use std::time::{SystemTime, UNIX_EPOCH};
2021-12-13 02:29:02 +08:00
2021-12-14 15:54:24 +08:00
/// Define the `profiles.yaml` schema
2021-12-13 02:29:02 +08:00
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
2022-01-21 02:57:15 +08:00
pub struct Profiles {
2021-12-13 02:29:02 +08:00
/// current profile's name
2021-12-18 02:29:54 +08:00
pub current: Option<usize>,
2021-12-13 02:29:02 +08:00
/// profile list
2021-12-14 00:40:41 +08:00
pub items: Option<Vec<ProfileItem>>,
2021-12-13 02:29:02 +08:00
}
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
2021-12-14 00:40:41 +08:00
pub struct ProfileItem {
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
/// current mode
2022-02-28 01:34:25 +08:00
#[serde(skip_serializing_if = "Option::is_none")]
2021-12-13 02:29:02 +08:00
pub mode: 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")]
2021-12-13 02:29:02 +08:00
pub selected: Option<Vec<ProfileSelected>>,
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")]
2021-12-14 00:40:41 +08:00
pub extra: Option<ProfileExtra>,
2022-02-28 01:34:25 +08:00
/// updated time
pub updated: Option<usize>,
2021-12-13 02:29:02 +08:00
}
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct ProfileSelected {
pub name: Option<String>,
pub now: Option<String>,
}
#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)]
2021-12-14 00:40:41 +08:00
pub struct ProfileExtra {
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
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
/// the result from url
pub struct ProfileResponse {
pub name: String,
pub file: String,
pub data: String,
2022-02-19 00:09:36 +08:00
pub extra: Option<ProfileExtra>,
2021-12-14 22:33:42 +08:00
}
2022-01-05 02:00:59 +08:00
2022-01-21 02:57:15 +08:00
impl Profiles {
2022-01-05 02:00:59 +08:00
/// read the config from the file
pub fn read_file() -> Self {
2022-02-28 01:34:25 +08:00
config::read_yaml::<Profiles>(dirs::profiles_path())
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<()> {
let data = config::read_yaml::<Self>(dirs::profiles_path());
2022-01-05 02:00:59 +08:00
if data.current.is_none() {
2022-02-28 01:34:25 +08:00
bail!("failed to read profiles.yaml")
2022-01-05 02:00:59 +08:00
} else {
self.current = data.current;
self.items = data.items;
Ok(())
}
}
/// import the new profile from the url
/// and update the config file
2022-02-28 01:34:25 +08:00
pub fn import_from_url(&mut self, url: String, result: ProfileResponse) -> Result<()> {
2022-01-05 02:00:59 +08:00
// save the profile file
2022-02-28 01:34:25 +08:00
let path = dirs::app_profiles_dir().join(&result.file);
2022-01-05 02:00:59 +08:00
let file_data = result.data.as_bytes();
File::create(path).unwrap().write(file_data).unwrap();
// update `profiles.yaml`
2022-01-21 02:57:15 +08:00
let data = Profiles::read_file();
2022-01-05 02:00:59 +08:00
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),
2022-02-07 17:26:05 +08:00
desc: Some("imported url".into()),
2022-01-05 02:00:59 +08:00
file: Some(result.file),
mode: Some(format!("rule")),
url: Some(url),
selected: Some(vec![]),
2022-02-19 00:09:36 +08:00
extra: result.extra,
2022-01-05 02:00:59 +08:00
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
2022-02-28 01:34:25 +08:00
pub fn put_current(&mut self, index: usize) -> Result<()> {
2022-01-05 02:00:59 +08:00
let items = self.items.take().unwrap_or(vec![]);
if index >= items.len() {
2022-02-28 01:34:25 +08:00
bail!("the index out of bound");
2022-01-05 02:00:59 +08:00
}
self.items = Some(items);
self.current = Some(index);
self.save_file()
}
2022-02-07 17:26:05 +08:00
/// append new item
/// return the new item's index
2022-02-28 01:34:25 +08:00
pub fn append_item(&mut self, name: String, desc: String) -> Result<(usize, PathBuf)> {
2022-02-07 17:26:05 +08:00
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);
2022-02-28 01:34:25 +08:00
let path = dirs::app_profiles_dir().join(&file);
2022-02-07 17:26:05 +08:00
2022-02-18 23:57:13 +08:00
match File::create(&path).unwrap().write(tmpl::ITEM_CONFIG) {
2022-02-07 17:26:05 +08:00
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),
});
let index = items.len();
self.items = Some(items);
Ok((index, path))
}
2022-02-28 01:34:25 +08:00
Err(_) => bail!("failed to create file"),
2022-02-07 17:26:05 +08:00
}
}
2022-01-05 02:00:59 +08:00
/// update the target profile
/// and save to config file
/// only support the url item
2022-02-28 01:34:25 +08:00
pub fn update_item(&mut self, index: usize, result: ProfileResponse) -> Result<()> {
2022-01-05 02:00:59 +08:00
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();
2022-02-28 01:34:25 +08:00
let file_path = dirs::app_profiles_dir().join(file_path);
2022-01-05 02:00:59 +08:00
let file_data = result.data.as_bytes();
File::create(file_path).unwrap().write(file_data).unwrap();
items[index].name = Some(result.name);
2022-02-19 00:09:36 +08:00
items[index].extra = result.extra;
2022-01-05 02:00:59 +08:00
items[index].updated = Some(now);
self.items = Some(items);
self.save_file()
}
/// patch item
2022-02-28 01:34:25 +08:00
pub fn patch_item(&mut self, index: usize, profile: ProfileItem) -> Result<()> {
2022-01-05 02:00:59 +08:00
let mut items = self.items.take().unwrap_or(vec![]);
if index >= items.len() {
2022-02-28 01:34:25 +08:00
bail!("index out of range");
2022-01-05 02:00:59 +08:00
}
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
2022-02-28 01:34:25 +08:00
pub fn delete_item(&mut self, index: usize) -> Result<bool> {
2022-01-05 02:00:59 +08:00
let mut current = self.current.clone().unwrap_or(0);
let mut items = self.items.clone().unwrap_or(vec![]);
if index >= items.len() {
2022-02-28 01:34:25 +08:00
bail!("index out of range");
2022-01-05 02:00:59 +08:00
}
2022-02-17 01:58:12 +08:00
let mut rm_item = items.remove(index);
// delete the file
if let Some(file) = rm_item.file.take() {
2022-02-28 01:34:25 +08:00
let file_path = dirs::app_profiles_dir().join(file);
2022-02-17 01:58:12 +08:00
if file_path.exists() {
if let Err(err) = remove_file(file_path) {
log::error!("{err}");
}
}
}
2022-01-05 02:00:59 +08:00
2022-01-08 14:21:12 +08:00
let mut should_change = false;
2022-01-05 02:00:59 +08:00
if current == index {
current = 0;
2022-01-08 14:21:12 +08:00
should_change = true;
2022-01-05 02:00:59 +08:00
} else if current > index {
current = current - 1;
}
2022-01-08 14:21:12 +08:00
2022-01-05 02:00:59 +08:00
self.current = Some(current);
2022-01-08 14:21:12 +08:00
self.items = Some(items);
match self.save_file() {
Ok(_) => Ok(should_change),
Err(err) => Err(err),
}
2022-01-05 02:00:59 +08:00
}
2022-01-07 23:29:20 +08:00
/// activate current profile
2022-02-28 01:34:25 +08:00
pub fn activate(&self, clash: &Clash) -> Result<()> {
2022-01-07 23:29:20 +08:00
let current = self.current.unwrap_or(0);
match self.items.clone() {
Some(items) => {
if current >= items.len() {
2022-02-28 01:34:25 +08:00
bail!("the index out of bound");
2022-01-07 23:29:20 +08:00
}
let profile = items[current].clone();
2022-01-21 02:31:44 +08:00
let clash_config = clash.config.clone();
let clash_info = clash.info.clone();
2022-02-28 01:34:25 +08:00
2022-01-07 23:29:20 +08:00
tauri::async_runtime::spawn(async move {
let mut count = 5; // retry times
2022-02-28 01:34:25 +08:00
let mut err = None;
2022-01-07 23:29:20 +08:00
while count > 0 {
2022-01-21 02:31:44 +08:00
match activate_profile(&profile, &clash_config, &clash_info).await {
2022-01-07 23:29:20 +08:00
Ok(_) => return,
2022-02-28 01:34:25 +08:00
Err(e) => err = Some(e),
2022-01-07 23:29:20 +08:00
}
count -= 1;
}
2022-02-28 01:34:25 +08:00
log::error!("failed to activate for `{}`", err.unwrap());
2022-01-07 23:29:20 +08:00
});
Ok(())
}
2022-02-28 01:34:25 +08:00
None => bail!("empty profiles"),
2022-01-07 23:29:20 +08:00
}
}
}
/// put the profile to clash
2022-01-21 02:31:44 +08:00
pub async fn activate_profile(
profile_item: &ProfileItem,
clash_config: &Mapping,
clash_info: &ClashInfo,
2022-02-28 01:34:25 +08:00
) -> Result<()> {
2022-01-07 23:29:20 +08:00
// temp profile's path
2022-02-28 01:34:25 +08:00
let temp_path = dirs::profiles_temp_path();
2022-01-07 23:29:20 +08:00
// generate temp profile
{
let file_name = match profile_item.file.clone() {
Some(file_name) => file_name,
2022-02-28 01:34:25 +08:00
None => bail!("profile item should have `file` field"),
2022-01-07 23:29:20 +08:00
};
2022-02-28 01:34:25 +08:00
let file_path = dirs::app_profiles_dir().join(file_name);
2022-01-07 23:29:20 +08:00
if !file_path.exists() {
2022-02-28 01:34:25 +08:00
bail!(
2022-01-21 02:31:44 +08:00
"profile `{}` not exists",
file_path.as_os_str().to_str().unwrap()
2022-02-28 01:34:25 +08:00
);
2022-01-07 23:29:20 +08:00
}
2022-01-21 02:31:44 +08:00
// begin to generate the new profile config
let def_config = config::read_yaml::<Mapping>(file_path.clone());
2022-02-23 23:23:07 +08:00
// use the clash config except 5 keys below
let mut new_config = clash_config.clone();
2022-01-21 02:31:44 +08:00
2022-01-07 23:29:20 +08:00
// Only the following fields are allowed:
// proxies/proxy-providers/proxy-groups/rule-providers/rules
2022-01-21 02:31:44 +08:00
let valid_keys = vec![
2022-01-07 23:29:20 +08:00
"proxies",
"proxy-providers",
"proxy-groups",
"rule-providers",
"rules",
2022-01-21 02:31:44 +08:00
];
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
config::save_yaml(
temp_path.clone(),
&new_config,
Some("# Clash Verge Temp File"),
)?
};
2022-01-21 02:31:44 +08:00
let server = format!("http://{}/configs", clash_info.server.clone().unwrap());
2022-01-07 23:29:20 +08:00
let mut headers = HeaderMap::new();
headers.insert("Content-Type", "application/json".parse().unwrap());
2022-01-21 02:31:44 +08:00
if let Some(secret) = clash_info.secret.clone() {
2022-01-07 23:29:20 +08:00
headers.insert(
"Authorization",
format!("Bearer {}", secret).parse().unwrap(),
);
}
let mut data = HashMap::new();
data.insert("path", temp_path.as_os_str().to_str().unwrap());
2022-02-28 01:34:25 +08:00
let client = reqwest::ClientBuilder::new().no_proxy().build()?;
client
.put(server)
.headers(headers)
.json(&data)
.send()
.await?;
Ok(())
2022-01-05 02:00:59 +08:00
}