You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Nothing/src-tauri/src/storage.rs

284 lines
8.6 KiB

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<PathBuf>,
}
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<PathBuf> = 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> {
ProjectDirs::from(QUALIFIER, ORGANIZATION, APPLICATION)
}
pub fn plan_dir() -> Option<PathBuf> {
Some(Self::proj_dir()?.data_dir().join(SAVED_PLANS))
}
pub fn history_dir() -> Option<PathBuf> {
Some(Self::proj_dir()?.data_dir().join(SAVED_HISTORIES))
}
pub fn config_file() -> Option<PathBuf> {
Some(Self::proj_dir()?.data_dir().join(CONFIG_FILE))
}
pub fn history_cache_file() -> Option<PathBuf> {
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<T: Into<String>>(file: T, plan: Plan) -> Result<(), Box<dyn Error>> {
std::fs::write(&file.into(), serde_json::to_string(&plan)?)?;
Ok(())
}
pub fn load_plan_at_path(path: PathBuf, save_local: bool) -> Option<Plan> {
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<PathBuf, Box<dyn Error>> {
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<PlanMetadata> {
let plan_dir = match Self::plan_dir() {
Some(dir) => dir,
None => return vec![],
};
let mut read_dir: Vec<DirEntry> = 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<String> {
// let file: PathBuf = Self::plan_dir()?.join(name).with_extension("json");
// Some(file.to_str()?.to_string())
// }
fn load_storage() -> Option<Self> {
let storage: Option<Storage> = 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<PlanMetadata> {
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<RunHistory> {
serde_json::from_str(&std::fs::read_to_string(Self::history_uuid_to_path(uuid)?).ok()?).ok()
}
pub fn load_cache() -> Option<HashMap<String, RunHistoryMetadata>> {
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<PathBuf> {
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();
}
}