Skip to main content

worgl_client/
store.rs

1//! Where a purse keeps its notes between runs.
2
3use std::fmt;
4use std::fs::{self, File, OpenOptions};
5use std::io::{self, Write};
6use std::path::{Path, PathBuf};
7use std::sync::Mutex;
8
9use worgl_sdk::Holder;
10
11use crate::error::Error;
12
13/// Where a [`Purse`](crate::Purse) saves its [`Holder`] after every change.
14///
15/// The JSON holds the secret key of every note, and notes are bearer:
16/// whoever reads it can spend them. Keep it somewhere private, encrypted at
17/// rest.
18pub trait Store: Send + Sync {
19    /// Replaces what was saved with `json`. Returns only once the data
20    /// would survive a crash: the purse hands out keys and transfers only
21    /// after this succeeds.
22    fn save(&self, json: &str) -> io::Result<()>;
23
24    /// Locks the store against other processes and reads what is saved in
25    /// it, for a store more than one process writes. A purse calls it
26    /// before every change: it makes the change to what was read here,
27    /// saves, and only then drops the lock, so it never saves over a change
28    /// another process made since it last looked.
29    ///
30    /// The default returns `None`, for a store only one purse writes: the
31    /// purse's own holder is then always current.
32    fn lock(&self) -> io::Result<Option<Locked>> {
33        Ok(None)
34    }
35}
36
37/// A store locked against other processes until this is dropped, and what
38/// was saved in it when the lock was taken.
39pub struct Locked {
40    saved: Option<String>,
41    _lock: Box<dyn Send>,
42}
43
44impl Locked {
45    /// What [`Store::lock`] returns: `saved` is the JSON saved now, or
46    /// `None` if nothing has been saved yet, and `lock` keeps other
47    /// processes out until it is dropped, such as a [`FileLock`].
48    pub fn new(saved: Option<String>, lock: impl Send + 'static) -> Self {
49        Self {
50            saved,
51            _lock: Box::new(lock),
52        }
53    }
54
55    /// The JSON saved when the lock was taken, if any.
56    pub fn saved(&self) -> Option<&str> {
57        self.saved.as_deref()
58    }
59}
60
61impl fmt::Debug for Locked {
62    // The JSON holds secret keys.
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        f.debug_struct("Locked").finish_non_exhaustive()
65    }
66}
67
68/// A store in one file, replaced whole on every save.
69///
70/// A save writes a temporary file beside it, flushes it to disk and renames
71/// it over the old one, so a crash leaves either the old store or the new,
72/// never a mix. On Unix the file is readable by its owner only.
73///
74/// Several processes can share it: [`lock`](Store::lock) takes a lock on a
75/// second file beside it, `.<name>.lock`, which stays there.
76#[derive(Debug, Clone)]
77pub struct FileStore {
78    path: PathBuf,
79}
80
81impl FileStore {
82    /// A store at `path`. Nothing is read or written yet.
83    pub fn new(path: impl Into<PathBuf>) -> Self {
84        Self { path: path.into() }
85    }
86
87    /// The file's path.
88    pub fn path(&self) -> &Path {
89        &self.path
90    }
91
92    /// Reads the saved holder, or a new empty one if the file does not
93    /// exist yet.
94    pub fn load(&self) -> Result<Holder, Error> {
95        match fs::read_to_string(&self.path) {
96            Ok(json) => Ok(Holder::from_json(&json)?),
97            Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(Holder::new()),
98            Err(error) => Err(error.into()),
99        }
100    }
101
102    /// Waits until no other process holds the store's lock, then takes it,
103    /// until the returned lock is dropped. [`lock`](Store::lock) does this
104    /// and reads the file; a store that wraps a `FileStore`, such as one
105    /// that encrypts it, can lock it this way and read it itself.
106    pub fn lock_file(&self) -> io::Result<FileLock> {
107        let mut options = OpenOptions::new();
108        options.read(true).write(true).create(true).truncate(false);
109        #[cfg(unix)]
110        std::os::unix::fs::OpenOptionsExt::mode(&mut options, 0o600);
111        let file = options.open(self.beside("lock"))?;
112        file.lock()?;
113        Ok(FileLock(file))
114    }
115
116    fn temporary(&self) -> PathBuf {
117        self.beside("new")
118    }
119
120    /// `.<name>.<suffix>` in the store's directory.
121    fn beside(&self, suffix: &str) -> PathBuf {
122        let name = self
123            .path
124            .file_name()
125            .map_or_else(|| "store".into(), |name| name.to_string_lossy());
126        self.path.with_file_name(format!(".{name}.{suffix}"))
127    }
128}
129
130/// A [`FileStore`]'s lock, held until it is dropped.
131#[derive(Debug)]
132pub struct FileLock(File);
133
134impl Drop for FileLock {
135    fn drop(&mut self) {
136        // A child process started meanwhile holds a copy of every open file
137        // until it execs, and a lock lasts until every copy of its file is
138        // closed. Unlocking ends it for all copies at once.
139        let _ = self.0.unlock();
140    }
141}
142
143impl Store for FileStore {
144    fn save(&self, json: &str) -> io::Result<()> {
145        let temporary = self.temporary();
146        let mut options = OpenOptions::new();
147        options.write(true).create(true).truncate(true);
148        #[cfg(unix)]
149        std::os::unix::fs::OpenOptionsExt::mode(&mut options, 0o600);
150        let mut file = options.open(&temporary)?;
151        file.write_all(json.as_bytes())?;
152        file.sync_all()?;
153        drop(file);
154        fs::rename(&temporary, &self.path)?;
155        // The rename is durable only once the directory is flushed too.
156        #[cfg(unix)]
157        {
158            let dir = match self.path.parent() {
159                Some(dir) if !dir.as_os_str().is_empty() => dir,
160                _ => Path::new("."),
161            };
162            fs::File::open(dir)?.sync_all()?;
163        }
164        Ok(())
165    }
166
167    fn lock(&self) -> io::Result<Option<Locked>> {
168        let lock = self.lock_file()?;
169        let saved = match fs::read_to_string(&self.path) {
170            Ok(json) => Some(json),
171            Err(error) if error.kind() == io::ErrorKind::NotFound => None,
172            Err(error) => return Err(error),
173        };
174        Ok(Some(Locked::new(saved, lock)))
175    }
176}
177
178/// A store in memory, lost when the process ends. For tests, and for
179/// applications that persist the JSON themselves.
180#[derive(Debug, Default)]
181pub struct MemoryStore {
182    saved: Mutex<Option<String>>,
183}
184
185impl MemoryStore {
186    /// An empty store.
187    pub fn new() -> Self {
188        Self::default()
189    }
190
191    /// The JSON saved last, if any.
192    pub fn saved(&self) -> Option<String> {
193        self.saved.lock().unwrap_or_else(|e| e.into_inner()).clone()
194    }
195}
196
197impl Store for MemoryStore {
198    fn save(&self, json: &str) -> io::Result<()> {
199        *self.saved.lock().unwrap_or_else(|e| e.into_inner()) = Some(json.to_owned());
200        Ok(())
201    }
202}
203
204impl<S: Store + ?Sized> Store for std::sync::Arc<S> {
205    fn save(&self, json: &str) -> io::Result<()> {
206        (**self).save(json)
207    }
208
209    fn lock(&self) -> io::Result<Option<Locked>> {
210        (**self).lock()
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use std::sync::mpsc;
217    use std::thread;
218    use std::time::Duration;
219
220    use super::*;
221
222    #[test]
223    fn a_file_store_is_locked_by_one_writer_at_a_time() {
224        let dir = tempfile::tempdir().unwrap();
225        let store = FileStore::new(dir.path().join("purse.json"));
226        let first = store.lock().unwrap().unwrap();
227        assert_eq!(first.saved(), None);
228
229        let (sender, receiver) = mpsc::channel();
230        let other = store.clone();
231        let waiter = thread::spawn(move || {
232            let locked = other.lock().unwrap().unwrap();
233            sender.send(locked.saved().map(str::to_owned)).unwrap();
234        });
235        assert!(receiver.recv_timeout(Duration::from_millis(200)).is_err());
236        store.save("{}").unwrap();
237        drop(first);
238        let saved = receiver.recv_timeout(Duration::from_secs(10)).unwrap();
239        assert_eq!(saved.as_deref(), Some("{}"));
240        waiter.join().unwrap();
241    }
242}