use std::collections::HashMap; use std::error::Error; use std::fs::DirEntry; use std::path::PathBuf; use directories::ProjectDirs; use serde::{Deserialize, Serialize}; use crate::config::Config; use crate::plan::{convert_old, Plan, PlanMetadata}; use crate::time::{RunHistory, RunHistoryMetadata}; #[derive(Serialize, Deserialize, Debug)] pub struct Storage { pub config: Config, pub last_plan: Option, } const QUALIFIER: &'static str = "me"; const ORGANIZATION: &'static str = "isark.poe"; const APPLICATION: &'static str = "Nothing"; const CONFIG_FILE: &'static str = "configuration.json"; const HISTORY_CACHE_FILE: &'static str = "history_cache.json"; const SAVED_PLANS: &'static str = "plans"; const SAVED_HISTORIES: &'static str = "histories"; fn mkdir_for_storage() { let dir_structure = Storage::plan_dir(); if let Some(dir_structure) = dir_structure { std::fs::create_dir_all(dir_structure) .map_err(|_e| log::error!("Could not create directory for storing config and saves")) .ok(); } let dir_structure: Option = Storage::history_dir(); if let Some(dir_structure) = dir_structure { std::fs::create_dir_all(dir_structure) .map_err(|_e| log::error!("Could not create directory for storing config and saves")) .ok(); } } impl Default for Storage { fn default() -> Self { mkdir_for_storage(); let storage = Self::load_storage(); match storage { Some(storage) => storage, None => Self { config: Default::default(), last_plan: None, }, } } } impl Storage { pub fn proj_dir() -> Option { ProjectDirs::from(QUALIFIER, ORGANIZATION, APPLICATION) } pub fn plan_dir() -> Option { Some(Self::proj_dir()?.data_dir().join(SAVED_PLANS)) } pub fn history_dir() -> Option { Some(Self::proj_dir()?.data_dir().join(SAVED_HISTORIES)) } pub fn config_file() -> Option { Some(Self::proj_dir()?.data_dir().join(CONFIG_FILE)) } pub fn history_cache_file() -> Option { Some(Self::proj_dir()?.data_dir().join(HISTORY_CACHE_FILE)) } pub fn save_config(&self) { let content = match serde_json::to_string_pretty(&self) { Ok(content) => content, Err(_) => return, }; let config_file = match Self::config_file() { Some(config_file) => config_file, None => return, }; match std::fs::write(&config_file, content) { Ok(_) => { if let Some(c) = config_file.to_str() { log::info!("Saved config to {c}"); } } Err(_) => log::error!("Could not write config"), } } pub fn save_plan_at_path>(file: T, plan: Plan) -> Result<(), Box> { std::fs::write(&file.into(), serde_json::to_string(&plan)?)?; Ok(()) } pub fn load_plan_at_path(path: PathBuf, save_local: bool) -> Option { let mut plan: Plan = match serde_json::from_str(&std::fs::read_to_string(&path).ok()?).ok() { Some(plan) => plan, None => convert_old(path.clone())?, }; if save_local { match Self::save_plan_at_store_path(path.file_name()?.to_str()?, plan.clone(), false) { Ok(path) => plan.set_stored_path(Some(path)), Err(_e) => { log::error!("Could not save plan at store path during load"); } } } //QoL touch file to put recent plans at top! :D match std::fs::File::open(&path) { Ok(file) => { file.set_modified(std::time::SystemTime::now()).ok(); } Err(_) => (), } Some(plan) } pub fn save_plan_at_store_path( file_name: &str, mut plan: Plan, allow_overwrite: bool, ) -> Result> { let plan_dir = match Self::plan_dir() { Some(dir) => dir, None => return Err("No plan dir".into()), }; let file_path = plan_dir.join(file_name).with_extension("json"); //Disallow overwriting. if !allow_overwrite && file_path.exists() { return Err("File already exists".into()); } if allow_overwrite && file_path.exists() { log::info!("Overwriting plan : {file_path:?}"); } std::fs::write(&file_path, serde_json::to_string(&plan)?)?; Ok(file_path.clone()) } pub fn enumerate_plans() -> Vec { let plan_dir = match Self::plan_dir() { Some(dir) => dir, None => return vec![], }; let mut read_dir: Vec = match plan_dir.read_dir() { Ok(read_dir) => read_dir.filter_map(|v| { log::trace!("Read dir: {:?}", v); v.ok() }).collect(), Err(_) => return vec![], }; read_dir.sort_by_key(|v| v.metadata().ok()?.modified().ok()); read_dir.reverse(); read_dir .iter() .filter_map(|entry| { let path = entry.path(); if path.extension()? != "json" { return None; } Self::load_metadata_at_path(path) }) .collect() } //TODO: Remove if this turns out to be unnecessary. // pub fn create_path_for_local_plan(name: &str, plan: &Plan) -> Option { // let file: PathBuf = Self::plan_dir()?.join(name).with_extension("json"); // Some(file.to_str()?.to_string()) // } fn load_storage() -> Option { let storage: Option = serde_json::from_str( &std::fs::read_to_string(Self::proj_dir()?.data_dir().join(CONFIG_FILE)).ok()?, ) .ok(); log::trace!("Loaded storage: {:?}", storage); storage } fn load_metadata_at_path(path: PathBuf) -> Option { let mut plan: PlanMetadata = match serde_json::from_str(&std::fs::read_to_string(&path).ok()?) { Ok(plan) => plan, Err(e) => { log::error!("Could not load metadata at path: {path:?} : {e}"); return None; } }; plan.set_stored_path(Some(path)); Some(plan) } pub fn save_history(history: RunHistory) { Self::store_history_at_path(&history); Self::store_history_at_cache(history); } pub fn load_history_at_uuid(uuid: String) -> Option { serde_json::from_str(&std::fs::read_to_string(Self::history_uuid_to_path(uuid)?).ok()?).ok() } pub fn load_cache() -> Option> { let path = match Self::history_cache_file() { Some(path) => path, None => { log::error!("Could not get path for history cache"); return None; }, }; serde_json::from_str(&std::fs::read_to_string(path).ok()?).ok() } fn history_uuid_to_path(uuid: String) -> Option { let history_dir = match Self::history_dir() { Some(dir) => dir, None => return None, }; Some(history_dir.join(uuid.to_string()).with_extension("json")) } fn store_history_at_path(history: &RunHistory) { let path = match Self::history_uuid_to_path(history.uuid()) { Some(path) => path, None => { log::error!("Could not get path for history"); return; }, }; let serialization_result = match serde_json::to_string(&history) { Ok(serialization_result) => serialization_result, Err(e) => { log::error!("Could not serialize history: {}", e); return;}, }; std::fs::write( path, serialization_result, ).ok(); } fn store_history_at_cache(history: RunHistory) { let mut cache = match Self::load_cache() { Some(cache) => cache, None => HashMap::new(), }; cache.insert(history.uuid(), history.into()); let path = match Self::history_cache_file() { Some(path) => path, None => { log::error!("Could not get path for history cache"); return; }, }; let serialization = match serde_json::to_string(&cache) { Ok(serialization) => serialization, Err(e) => { log::error!("Could not serialize history cache: {}", e); return; }, }; std::fs::write(path, serialization).ok(); } }