1use std::{fs, io::{self, BufReader, BufWriter}, path::{Path, PathBuf}, time::{Duration, SystemTime}};
2
3use serde::{de::DeserializeOwned, Serialize};
4
5#[derive(Clone, Debug)]
6pub struct FileBackedValue {
7 path: PathBuf,
8 dirty_time: Option<Duration>,
9}
10
11#[derive(Debug)]
12pub enum FileBackedValueError {
13 FileError(io::Error),
14 JsonError(serde_json::Error),
15}
16
17pub type FileBackedValueResult<T> = Result<T, FileBackedValueError>;
18
19impl FileBackedValue
20{
21 pub fn new(filename: &str) -> Self {
28 let parent = PathBuf::from(directories::BaseDirs::new().expect("No valid home directory found").data_dir());
29 let filename = sanitize_filename::sanitize(filename);
30 Self {
31 path: parent.join(filename),
32 dirty_time: None,
33 }
34 }
35
36 pub fn new_at(filename: &str, parent: &Path) -> Self {
38 let parent = PathBuf::from(parent);
39 let filename = sanitize_filename::sanitize(filename);
40 Self {
41 path: parent.join(filename),
42 dirty_time: None,
43 }
44 }
45
46 pub fn set_dirty_time(&mut self, dirty_time: Duration) {
48 self.dirty_time = Some(dirty_time);
49 }
50
51 pub fn path(&self) -> &PathBuf {
53 &self.path
54 }
55
56 pub fn clear(&mut self) -> io::Result<()> {
58 fs::remove_file(self.path())
59 }
60
61 pub fn get<T>(&mut self) -> FileBackedValueResult<Option<T>>
63 where
64 T: DeserializeOwned
65 {
66 if self.file_is_dirty() {
67 Ok(None)
68 } else {
69 read_file(&self.path)
70 }
71 }
72
73 pub fn get_or_insert<T>(&mut self, default: T) -> FileBackedValueResult<T>
75 where
76 T: DeserializeOwned + Serialize
77 {
78 if self.file_is_dirty() {
79 self.insert(&default);
80 Ok(default)
81 } else {
82 let res: Option<T> = read_file(&self.path)?;
85 Ok(res.unwrap())
86 }
87 }
88
89 pub fn get_or_insert_with<F, T>(&mut self, default: F) -> FileBackedValueResult<T>
91 where
92 F: FnOnce() -> T,
93 T: DeserializeOwned + Serialize,
94 {
95 if self.file_is_dirty() {
96 let res = default();
97 self.insert(&res);
98 Ok(res)
99 } else {
100 let res: Option<T> = read_file(&self.path)?;
103 Ok(res.unwrap())
104 }
105 }
106
107 pub fn insert<T>(&mut self, value: &T)
109 where
110 T: Serialize
111 {
112 write_file(&self.path, value).unwrap();
113 }
114
115 fn file_is_dirty(&self) -> bool {
118 self.dirty_time.is_some_and(|dirty_time|
119 file_needs_recomputation(&self.path(), dirty_time))
120 }
121}
122
123fn read_file<T>(path: &PathBuf) -> FileBackedValueResult<Option<T>>
125where
126 T: DeserializeOwned
127{
128 match fs::OpenOptions::new().read(true).open(path) {
129 Ok(f) => {
130 let rdr = BufReader::new(f);
131 serde_json::from_reader(rdr)
132 .map_err(Into::into)
133 .map(|json| Some(json))
134 },
135 Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
136 Err(e) => Err(FileBackedValueError::FileError(e))
137 }
138}
139
140fn write_file<T>(path: &PathBuf, value: &T) -> FileBackedValueResult<()>
142where
143 T: Serialize
144{
145 if let Some(dir) = path.parent() {
147 fs::create_dir_all(dir).map_err(Into::into)?;
148 }
149
150 let file = fs::OpenOptions::new()
151 .create(true)
152 .write(true)
153 .truncate(true)
154 .open(path)
155 .map_err(Into::into)?;
156
157 let wtr = BufWriter::new(file);
158 serde_json::to_writer(wtr, value).map_err(Into::into)
159}
160
161fn file_needs_recomputation(path: &PathBuf, dirty_time: Duration) -> bool {
164 time_since_modified(path).is_none_or(|last_modified|
165 last_modified >= dirty_time)
166}
167
168fn time_since_modified(path: &PathBuf) -> Option<Duration> {
170 if let Ok(time) = fs::metadata(path) {
171 let now = SystemTime::now();
172 let last_modified = time.modified().or_else(|_| time.created()).ok()?;
173 now.duration_since(last_modified).ok()
174 } else {
175 None
176 }
177}
178
179impl Into<FileBackedValueError> for io::Error {
180 fn into(self) -> FileBackedValueError {
181 FileBackedValueError::FileError(self)
182 }
183}
184
185impl Into<FileBackedValueError> for serde_json::Error {
186 fn into(self) -> FileBackedValueError {
187 FileBackedValueError::JsonError(self)
188 }
189}