//! Общий слой персиста: атомарное чтение/запись JSON в каталоге конфигурации. //! Используется и настройками, и профилями. use serde::{de::DeserializeOwned, Serialize}; use std::path::PathBuf; use tauri::{AppHandle, Manager}; /// Полный путь к файлу в каталоге конфигурации (каталог гарантированно существует). pub fn path(app: &AppHandle, file: &str) -> Result { Ok(config_dir(app)?.join(file)) } /// Каталог конфигурации приложения, гарантируя его существование. /// На Windows — `%APPDATA%/com.unigate.app`. fn config_dir(app: &AppHandle) -> Result { let dir = app .path() .app_config_dir() .map_err(|e| format!("не удалось определить каталог конфигурации: {e}"))?; std::fs::create_dir_all(&dir) .map_err(|e| format!("не удалось создать каталог конфигурации: {e}"))?; Ok(dir) } /// Читает JSON-файл. Если файла нет — `T::default()`. /// Битый файл не валит приложение: логируем и откатываемся на дефолт. pub fn read_json(app: &AppHandle, file: &str) -> Result { let path = config_dir(app)?.join(file); if !path.exists() { return Ok(T::default()); } let raw = std::fs::read_to_string(&path) .map_err(|e| format!("не удалось прочитать {}: {e}", path.display()))?; match serde_json::from_str::(&raw) { Ok(value) => Ok(value), Err(e) => { eprintln!("{file} повреждён ({e}), откат на значения по умолчанию"); Ok(T::default()) } } } /// Атомарно (temp + rename) пишет значение в JSON-файл. pub fn write_json( app: &AppHandle, file: &str, value: &T, ) -> Result<(), String> { let path = config_dir(app)?.join(file); let json = serde_json::to_string_pretty(value) .map_err(|e| format!("не удалось сериализовать {file}: {e}"))?; let tmp = path.with_extension("tmp"); std::fs::write(&tmp, json).map_err(|e| format!("не удалось записать {}: {e}", tmp.display()))?; std::fs::rename(&tmp, &path) .map_err(|e| format!("не удалось заменить {}: {e}", path.display()))?; Ok(()) }