use dirs::home_dir;
use rayon::prelude::*;
use sanitize_filename::sanitize;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::marker::PhantomData;
use std::{
collections::BTreeSet,
fs::{self, read_to_string},
io::Write,
path::{Path, PathBuf},
};
use uuid::Uuid;
#[derive(Serialize, Deserialize)]
struct CacheInfo<T: FsLoad> {
path: PathBuf,
dependents: BTreeSet<Uuid>,
#[serde(skip)]
_marker: PhantomData<T>,
}
impl<T: FsLoad> CacheInfo<T> {
fn new(path: PathBuf) -> Self {
Self {
path,
dependents: BTreeSet::default(),
_marker: PhantomData,
}
}
fn cache_dependents(ty: &T) {
for dpy in ty.dependencies() {
if let Some(mut info) = Self::load(dpy) {
if info.dependents.insert(ty.id()) {
info.persist(dpy);
} else {
}
}
}
}
fn cache_path(id: Uuid, path: PathBuf) {
match Self::load(id) {
Some(mut info) => {
if &info.path != &path {
info.path = path;
info.persist(id);
}
}
None => {
Self::new(path).persist(id);
}
}
}
fn load(id: Uuid) -> Option<Self> {
let cache_path = Self::cache_dir().join(id.to_string());
let s: String = read_to_string(&cache_path).ok()?;
let info: Self = toml::from_str(&s).ok()?;
Some(info)
}
fn cache_dir() -> PathBuf {
let p = home_dir().unwrap().join(".cache").join(T::type_name());
std::fs::create_dir_all(&p).unwrap();
p
}
fn persist(self, id: Uuid) {
let p = Self::cache_dir().join(id.to_string());
let s: String = toml::to_string(&self).unwrap();
let mut f = fs::File::create(&p).unwrap();
f.write_all(&mut s.as_bytes()).unwrap();
}
}
pub trait FsLoad: Serialize + DeserializeOwned + Send {
fn id(&self) -> Uuid;
fn dependencies(&self) -> BTreeSet<Uuid> {
Default::default()
}
fn dependents(&self) -> BTreeSet<Uuid> {
Self::verify_dependents::<Self>(self.id());
let info: Option<CacheInfo<Self>> = CacheInfo::load(self.id());
info.map(|info| info.dependents).unwrap_or_default()
}
fn save(&self) {
let path = Self::load_path_with_cache::<Self>(Self::save_paths(), self.id())
.unwrap_or_else(|| Self::save_paths()[0].join(self.file_name()));
let s: String = toml::to_string(&self).unwrap();
let mut file = std::fs::File::create(&path).unwrap();
file.write_all(&mut s.as_bytes()).unwrap();
CacheInfo::<Self>::cache_path(self.id(), path);
CacheInfo::cache_dependents(self);
}
fn save_at(&self, dir: &Path) {
assert!(
dir.is_dir(),
"pass in the directory where the file should reside"
);
assert!(
CacheInfo::<Self>::load(self.id()).is_none(),
"card already exists"
);
let base_filename = sanitize(&self.file_name()).replace(" ", "_");
let mut path = dir.join(&base_filename);
path.set_extension("toml");
let mut counter = 1;
while path.exists() {
let new_name = format!("{}({})", &base_filename, counter);
path = dir.join(&new_name);
path.set_extension("toml");
counter += 1;
}
let s: String = toml::to_string(&self).unwrap();
let mut file = std::fs::File::create(&path).unwrap();
file.write_all(&mut s.as_bytes()).unwrap();
CacheInfo::<Self>::cache_path(self.id(), path.to_path_buf());
CacheInfo::cache_dependents(self);
}
fn file_name(&self) -> String {
self.id().to_string()
}
fn type_name() -> String {
std::any::type_name::<Self>()
.to_string()
.split("::")
.last()
.unwrap()
.to_string()
}
fn path(&self) -> Option<PathBuf> {
Self::load_path_with_cache::<Self>(Self::save_paths(), self.id())
}
fn load(id: Uuid) -> Option<Self> {
let path = Self::load_path_with_cache::<Self>(Self::save_paths(), id)?;
let s = read_to_string(&path).unwrap();
let s: Self = toml::from_str(&s).unwrap();
CacheInfo::cache_dependents(&s);
Some(s)
}
fn load_all() -> Vec<Self> {
let mut vec = vec![];
for p in Self::save_paths() {
for obj in load_all(&p) {
vec.push(obj);
}
}
vec
}
fn save_paths() -> Vec<PathBuf> {
let path = dirs::home_dir()
.unwrap()
.join(".local")
.join("share")
.join(Self::type_name());
std::fs::create_dir_all(&path).unwrap();
vec![path]
}
fn try_load(id: Uuid) -> Option<PathBuf> {
CacheInfo::<Self>::load(id)
.map(|info| info.path)
.filter(|path| path.exists())
}
fn verify_dependents<T: FsLoad>(id: Uuid) {
if let Some(mut info) = CacheInfo::<Self>::load(id) {
let len = info.dependents.len();
info.dependents
.retain(|dependent| T::load(*dependent).unwrap().dependencies().contains(&id));
if info.dependents.len() != len {
info.persist(id);
}
}
}
fn load_path_with_cache<T: FsLoad>(dir_paths: Vec<PathBuf>, id: Uuid) -> Option<PathBuf> {
match Self::try_load(id) {
some @ Some(_) => some,
None => {
Self::sync_cache::<T>(dir_paths);
Self::try_load(id)
}
}
}
fn sync_cache<T: FsLoad>(dir_paths: Vec<PathBuf>) {
let mut vec = vec![];
for dir_path in dir_paths {
for path in get_all_file_paths(&dir_path) {
let s = std::fs::read_to_string(&path).unwrap();
let obj: T = toml::from_str(&s).unwrap();
CacheInfo::<Self>::cache_path(obj.id(), path);
vec.push(obj);
}
}
for o in vec {
CacheInfo::cache_dependents(&o);
}
}
}
fn load_all<T: DeserializeOwned + FsLoad + Send>(dir: &Path) -> Vec<T> {
let paths = get_all_file_paths(dir);
let vec: Vec<T> = paths
.into_par_iter()
.map(|path| {
let s = std::fs::read_to_string(&path).unwrap();
let obj: T = toml::from_str(&s).unwrap();
CacheInfo::<T>::cache_path(obj.id(), path);
obj
})
.collect();
for o in &vec {
CacheInfo::cache_dependents(o);
}
vec
}
fn get_all_file_paths(root: &Path) -> Vec<PathBuf> {
walkdir::WalkDir::new(root)
.into_iter()
.filter_map(|entry| entry.ok())
.filter_map(|entry| {
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "toml") {
Some(path.to_path_buf())
} else {
None
}
})
.par_bridge()
.collect()
}