use std::{fs, io::{self, BufReader, BufWriter}, path::{Path, PathBuf}, time::{Duration, SystemTime}};
use serde::{de::DeserializeOwned, Serialize};
#[derive(Clone, Debug)]
pub struct FileBackedValue {
path: PathBuf,
dirty_time: Option<Duration>,
}
#[derive(Debug)]
pub enum FileBackedValueError {
FileError(io::Error),
JsonError(serde_json::Error),
}
pub type FileBackedValueResult<T> = Result<T, FileBackedValueError>;
impl FileBackedValue
{
pub fn new(filename: &str) -> Self {
let parent = PathBuf::from(directories::BaseDirs::new().expect("No valid home directory found").data_dir());
let filename = sanitize_filename::sanitize(filename);
Self {
path: parent.join(filename),
dirty_time: None,
}
}
pub fn new_at(filename: &str, parent: &Path) -> Self {
let parent = PathBuf::from(parent);
let filename = sanitize_filename::sanitize(filename);
Self {
path: parent.join(filename),
dirty_time: None,
}
}
pub fn set_dirty_time(&mut self, dirty_time: Duration) {
self.dirty_time = Some(dirty_time);
}
pub fn path(&self) -> &PathBuf {
&self.path
}
pub fn clear(&mut self) -> io::Result<()> {
fs::remove_file(self.path())
}
pub fn get<T>(&mut self) -> FileBackedValueResult<Option<T>>
where
T: DeserializeOwned
{
if self.file_is_dirty() {
Ok(None)
} else {
read_file(&self.path)
}
}
pub fn get_or_insert<T>(&mut self, default: T) -> FileBackedValueResult<T>
where
T: DeserializeOwned + Serialize
{
if self.file_is_dirty() {
self.insert(&default);
Ok(default)
} else {
let res: Option<T> = read_file(&self.path)?;
Ok(res.unwrap())
}
}
pub fn get_or_insert_with<F, T>(&mut self, default: F) -> FileBackedValueResult<T>
where
F: FnOnce() -> T,
T: DeserializeOwned + Serialize,
{
if self.file_is_dirty() {
let res = default();
self.insert(&res);
Ok(res)
} else {
let res: Option<T> = read_file(&self.path)?;
Ok(res.unwrap())
}
}
pub fn insert<T>(&mut self, value: &T)
where
T: Serialize
{
write_file(&self.path, value).unwrap();
}
fn file_is_dirty(&self) -> bool {
self.dirty_time.is_some_and(|dirty_time|
file_needs_recomputation(&self.path(), dirty_time))
}
}
fn read_file<T>(path: &PathBuf) -> FileBackedValueResult<Option<T>>
where
T: DeserializeOwned
{
match fs::OpenOptions::new().read(true).open(path) {
Ok(f) => {
let rdr = BufReader::new(f);
serde_json::from_reader(rdr)
.map_err(Into::into)
.map(|json| Some(json))
},
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(FileBackedValueError::FileError(e))
}
}
fn write_file<T>(path: &PathBuf, value: &T) -> FileBackedValueResult<()>
where
T: Serialize
{
if let Some(dir) = path.parent() {
fs::create_dir_all(dir).map_err(Into::into)?;
}
let file = fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(path)
.map_err(Into::into)?;
let wtr = BufWriter::new(file);
serde_json::to_writer(wtr, value).map_err(Into::into)
}
fn file_needs_recomputation(path: &PathBuf, dirty_time: Duration) -> bool {
time_since_modified(path).is_none_or(|last_modified|
last_modified >= dirty_time)
}
fn time_since_modified(path: &PathBuf) -> Option<Duration> {
if let Ok(time) = fs::metadata(path) {
let now = SystemTime::now();
let last_modified = time.modified().or_else(|_| time.created()).ok()?;
now.duration_since(last_modified).ok()
} else {
None
}
}
impl Into<FileBackedValueError> for io::Error {
fn into(self) -> FileBackedValueError {
FileBackedValueError::FileError(self)
}
}
impl Into<FileBackedValueError> for serde_json::Error {
fn into(self) -> FileBackedValueError {
FileBackedValueError::JsonError(self)
}
}