|
|
use std::{
|
|
|
collections::BTreeMap,
|
|
|
env,
|
|
|
fs,
|
|
|
path::{Path, PathBuf},
|
|
|
sync::{Mutex, OnceLock},
|
|
|
};
|
|
|
|
|
|
use serde_json::{Map, Value};
|
|
|
|
|
|
const APP_NAME: &str = "com.ziyun.noticeclient";
|
|
|
const CONFIG_FILE_NAME: &str = "config.json";
|
|
|
|
|
|
fn get_home_dir() -> PathBuf {
|
|
|
if let Ok(value) = env::var("HOME") {
|
|
|
return PathBuf::from(value);
|
|
|
}
|
|
|
|
|
|
if let Ok(value) = env::var("USERPROFILE") {
|
|
|
return PathBuf::from(value);
|
|
|
}
|
|
|
|
|
|
PathBuf::from(".")
|
|
|
}
|
|
|
|
|
|
fn get_config_dir() -> PathBuf {
|
|
|
if cfg!(target_os = "linux") {
|
|
|
if let Ok(value) = env::var("XDG_CONFIG_HOME") {
|
|
|
return PathBuf::from(value).join(APP_NAME);
|
|
|
}
|
|
|
|
|
|
return get_home_dir().join(".config").join(APP_NAME);
|
|
|
}
|
|
|
|
|
|
if let Ok(value) = env::var("APPDATA") {
|
|
|
return PathBuf::from(value).join(APP_NAME);
|
|
|
}
|
|
|
|
|
|
get_home_dir().join(".").join(APP_NAME)
|
|
|
}
|
|
|
|
|
|
fn get_config_file_path() -> PathBuf {
|
|
|
get_config_dir().join(CONFIG_FILE_NAME)
|
|
|
}
|
|
|
|
|
|
fn ensure_parent_dir(path: &Path) -> Result<(), String> {
|
|
|
let Some(parent) = path.parent() else {
|
|
|
return Ok(());
|
|
|
};
|
|
|
|
|
|
fs::create_dir_all(parent).map_err(|error| format!("创建配置目录失败: {error}"))
|
|
|
}
|
|
|
|
|
|
fn read_config() -> Result<Value, String> {
|
|
|
let path = get_config_file_path();
|
|
|
|
|
|
if !path.exists() {
|
|
|
return Ok(Value::Object(Map::new()));
|
|
|
}
|
|
|
|
|
|
let content = fs::read_to_string(&path).map_err(|error| format!("读取配置文件失败: {error}"))?;
|
|
|
if content.trim().is_empty() {
|
|
|
return Ok(Value::Object(Map::new()));
|
|
|
}
|
|
|
|
|
|
serde_json::from_str::<Value>(&content).map_err(|error| format!("解析配置文件失败: {error}"))
|
|
|
}
|
|
|
|
|
|
fn write_config(value: &Value) -> Result<(), String> {
|
|
|
let path = get_config_file_path();
|
|
|
ensure_parent_dir(&path)?;
|
|
|
let content = serde_json::to_string_pretty(value).map_err(|error| format!("序列化配置失败: {error}"))?;
|
|
|
fs::write(path, content).map_err(|error| format!("写入配置文件失败: {error}"))
|
|
|
}
|
|
|
|
|
|
static CONFIG_IO_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
|
|
|
|
|
|
fn lock_config_io() -> Result<std::sync::MutexGuard<'static, ()>, String> {
|
|
|
let mutex = CONFIG_IO_MUTEX.get_or_init(|| Mutex::new(()));
|
|
|
mutex
|
|
|
.lock()
|
|
|
.map_err(|_| "配置文件锁异常(poisoned)".to_string())
|
|
|
}
|
|
|
|
|
|
fn merge_value(target: &mut Value, patch: Value) {
|
|
|
match (target, patch) {
|
|
|
(Value::Object(target_map), Value::Object(patch_map)) => {
|
|
|
for (key, value) in patch_map {
|
|
|
if let Some(existing) = target_map.get_mut(&key) {
|
|
|
merge_value(existing, value);
|
|
|
} else {
|
|
|
target_map.insert(key, value);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
(target_value, patch_value) => {
|
|
|
*target_value = patch_value;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
#[tauri::command]
|
|
|
pub fn config_get_all() -> Result<BTreeMap<String, Value>, String> {
|
|
|
let _guard = lock_config_io()?;
|
|
|
let value = read_config()?;
|
|
|
let Value::Object(map) = value else {
|
|
|
return Ok(BTreeMap::new());
|
|
|
};
|
|
|
|
|
|
Ok(map.into_iter().collect())
|
|
|
}
|
|
|
|
|
|
pub fn merge_config_disk(partial: Value) -> Result<(), String> {
|
|
|
let _guard = lock_config_io()?;
|
|
|
let mut current = read_config()?;
|
|
|
merge_value(&mut current, partial);
|
|
|
write_config(¤t)?;
|
|
|
Ok(())
|
|
|
}
|
|
|
|
|
|
#[tauri::command]
|
|
|
pub fn config_merge(partial: Value) -> Result<BTreeMap<String, Value>, String> {
|
|
|
let _guard = lock_config_io()?;
|
|
|
let mut current = read_config()?;
|
|
|
merge_value(&mut current, partial);
|
|
|
write_config(¤t)?;
|
|
|
|
|
|
let Value::Object(map) = current else {
|
|
|
return Ok(BTreeMap::new());
|
|
|
};
|
|
|
|
|
|
Ok(map.into_iter().collect())
|
|
|
}
|