1use std::{
2 fs::{self, OpenOptions},
3 io::{self, BufReader, BufWriter},
4 path::{Path, PathBuf},
5 time::{Duration, SystemTime},
6};
7
8use serde::{de::DeserializeOwned, Serialize};
9
10pub struct FileBackedValue<T>
11 where T: Serialize + DeserializeOwned
12{
13 dir: PathBuf,
14 filename: String,
15 value: Option<T>,
16 dirty_time: Option<Duration>,
17}
18
19#[derive(Debug)]
20pub enum FileBackedValueError {
21 FileError(io::Error),
22 JsonError(serde_json::Error),
23}
24
25pub type FileBackedValueResult<T> = Result<T, FileBackedValueError>;
26
27impl<T> FileBackedValue<T>
28 where T: Serialize + DeserializeOwned
29{
30 pub fn new(filename: &str) -> Self {
31 Self {
32 dir: PathBuf::from(directories::BaseDirs::new().expect("No valid home directory found").data_dir()),
33 filename: sanitize_filename::sanitize(filename),
34 value: None,
35 dirty_time: None,
36 }
37 }
38
39 pub fn new_at(filename: &str, dir: &Path) -> Self {
40 Self {
41 dir: PathBuf::from(dir),
42 filename: sanitize_filename::sanitize(filename),
43 value: None,
44 dirty_time: None,
45 }
46 }
47
48 pub fn path(&self) -> PathBuf {
50 self.dir.join(&self.filename)
51 }
52
53 pub fn clear(&mut self) -> io::Result<()> {
55 self.value = None;
56 fs::remove_file(self.path())
57 }
58
59 pub fn set_dirty_time(&mut self, dirty_time: Duration) {
63 self.dirty_time = Some(dirty_time);
64 }
65
66 pub fn set_dirty(&mut self) -> Option<T> {
69 self.value.take()
70 }
71
72 pub fn get(&mut self) -> FileBackedValueResult<Option<&T>> {
74 if self.value.is_none() || self.file_is_dirty() {
75 self.value = self.read_file()?;
77 }
78 Ok(self.value.as_ref())
79 }
80
81 pub fn get_or_insert(&mut self, default: T) -> FileBackedValueResult<&T> {
82 if self.file_is_dirty() {
83 Ok(self.insert(default))
85 } else if self.value.is_none() {
86 let value = self.read_file()?.unwrap();
89 Ok(self.value.insert(value))
90 } else {
91 Ok(self.value.as_ref().unwrap())
93 }
94 }
95
96 pub fn get_or_insert_with<F>(&mut self, default: F) -> FileBackedValueResult<&T>
97 where F: FnOnce() -> T
98 {
99 if self.file_is_dirty() {
100 Ok(self.insert((default)()))
102 } else if self.value.is_none() {
103 let value = self.read_file()?.unwrap();
106 Ok(self.value.insert(value))
107 } else {
108 Ok(self.value.as_ref().unwrap())
110 }
111 }
112
113 pub fn insert(&mut self, value: T) -> &T {
116 self.write_file(&value).unwrap();
117 self.value.insert(value)
118 }
119
120 fn read_file(&self) -> FileBackedValueResult<Option<T>> {
122 match OpenOptions::new().read(true).open(self.path()) {
123 Ok(f) => {
124 let rdr = BufReader::new(f);
125 serde_json::from_reader(rdr)
126 .map_err(|e| FileBackedValueError::JsonError(e))
127 .map(|json| Some(json))
128 },
129 Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
130 Err(e) => Err(FileBackedValueError::FileError(e))
131 }
132 }
133
134 fn write_file(&self, value: &T) -> FileBackedValueResult<()> {
136 fs::create_dir_all(&self.dir)
138 .map_err(|e| FileBackedValueError::FileError(e))?;
139
140 let path = self.path();
141 let file = OpenOptions::new().create_new(true).write(true).truncate(true).open(path)
142 .map_err(|e| FileBackedValueError::FileError(e))?;
143 let wtr = BufWriter::new(file);
144 serde_json::to_writer(wtr, value)
145 .map_err(|e| FileBackedValueError::JsonError(e))
146 }
147
148 fn file_is_dirty(&self) -> bool {
151 self.dirty_time.is_some_and(|dirty_time|
152 file_needs_recomputation(&self.path(), dirty_time))
153 }
154}
155
156fn file_needs_recomputation(path: &Path, dirty_time: Duration) -> bool {
159 time_since_last_modified(path).is_none_or(|last_modified|
160 last_modified >= dirty_time)
161}
162
163fn time_since_last_modified(path: &Path) -> Option<Duration> {
165 if let Ok(time) = fs::metadata(path) {
166 let now = SystemTime::now();
167 let last_modified = time.modified().ok()?;
168 now.duration_since(last_modified).ok()
169 } else {
170 None
171 }
172}