Skip to main content

file_backed_value/
lib.rs

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    /// Path to the backing file.
49    pub fn path(&self) -> PathBuf {
50        self.dir.join(&self.filename)
51    }
52
53    /// Clear the currently stored value and remove the backing file.
54    pub fn clear(&mut self) -> io::Result<()> {
55        self.value = None;
56        fs::remove_file(self.path())
57    }
58
59    /// If the time since the file was last edited is longer ago than `dirty_time`,
60    /// require a recomputation of the value and a writeback to the file.
61    /// If this value is not set, the file is only ever read once.
62    pub fn set_dirty_time(&mut self, dirty_time: Duration) {
63        self.dirty_time = Some(dirty_time);
64    }
65
66    /// Make this file dirty, requiring a recomputation the next time a value is get.
67    /// Returns the currently stored value, if any.
68    pub fn set_dirty(&mut self) -> Option<T> {
69        self.value.take()
70    }
71
72    /// Get the current value, which might be None if the backing file does not yet exist.
73    pub fn get(&mut self) -> FileBackedValueResult<Option<&T>> {
74        if self.value.is_none() || self.file_is_dirty() {
75            // The backing file has not been read before or has become dirty.
76            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            // If the file is dirty, recompute even if we already have a value.
84            Ok(self.insert(default))
85        } else if self.value.is_none() {
86            // The file has not been read before; read it now and store the value.
87            // The file must exists because otherwise it will have been marked as dirty.
88            let value = self.read_file()?.unwrap();
89            Ok(self.value.insert(value))
90        } else {
91            // The file is not dirty, return the current value if it exists.
92            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            // If the file is dirty, recompute even if we already have a value.
101            Ok(self.insert((default)()))
102        } else if self.value.is_none() {
103            // The file has not been read before; read it now and store the value.
104            // The file must exists because otherwise it will have been marked as dirty.
105            let value = self.read_file()?.unwrap();
106            Ok(self.value.insert(value))
107        } else {
108            // The file is not dirty, return the current value if it exists.
109            Ok(self.value.as_ref().unwrap())
110        }
111    }
112
113    /// Inserts `value` into the option and writes it to the backing file.
114    /// Returns a mutable reference to the value.
115    pub fn insert(&mut self, value: T) -> &T {
116        self.write_file(&value).unwrap();
117        self.value.insert(value)
118    }
119
120    /// Read a value of type `T` from the backing file as a JSON string.
121    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    /// Write `value` to the backing file as a JSON string.
135    fn write_file(&self, value: &T) -> FileBackedValueResult<()> {
136        // Create parent directories if necessary.
137        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    /// Check whether the backing file was last modified longer than `dirty_time` ago.
149    /// If the file does not exist or the modification time could otherwise not be retrieved, true is returned.
150    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
156/// Check whether the file at `path` was last modified longer than `dirty_time` ago.
157/// If the file does not exist or the modification time could otherwise not be retrieved, true is returned.
158fn 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
163/// Get the duration since the file at `path` was last modified.
164fn 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}