1use 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
13pub trait Store: Send + Sync {
19 fn save(&self, json: &str) -> io::Result<()>;
23
24 fn lock(&self) -> io::Result<Option<Locked>> {
33 Ok(None)
34 }
35}
36
37pub struct Locked {
40 saved: Option<String>,
41 _lock: Box<dyn Send>,
42}
43
44impl Locked {
45 pub fn new(saved: Option<String>, lock: impl Send + 'static) -> Self {
49 Self {
50 saved,
51 _lock: Box::new(lock),
52 }
53 }
54
55 pub fn saved(&self) -> Option<&str> {
57 self.saved.as_deref()
58 }
59}
60
61impl fmt::Debug for Locked {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64 f.debug_struct("Locked").finish_non_exhaustive()
65 }
66}
67
68#[derive(Debug, Clone)]
77pub struct FileStore {
78 path: PathBuf,
79}
80
81impl FileStore {
82 pub fn new(path: impl Into<PathBuf>) -> Self {
84 Self { path: path.into() }
85 }
86
87 pub fn path(&self) -> &Path {
89 &self.path
90 }
91
92 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 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 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#[derive(Debug)]
132pub struct FileLock(File);
133
134impl Drop for FileLock {
135 fn drop(&mut self) {
136 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 #[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#[derive(Debug, Default)]
181pub struct MemoryStore {
182 saved: Mutex<Option<String>>,
183}
184
185impl MemoryStore {
186 pub fn new() -> Self {
188 Self::default()
189 }
190
191 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}