light_magic/
atomic.rs

1use serde::{de::DeserializeOwned, Serialize};
2use std::{
3    ffi::{OsStr, OsString},
4    fmt,
5    fs::{self, File},
6    io::{self, BufWriter},
7    ops::{Deref, DerefMut},
8    path::{Path, PathBuf},
9    sync::{RwLock, RwLockReadGuard, RwLockWriteGuard},
10};
11use tracing::{error, info};
12
13/// This trait needs to be implemented for the Database struct.
14/// It requires a few implementations. The defined functions
15/// have default definitions.
16pub trait DataStore: Default + Serialize {
17    /// Opens a Database by the specified path. If the Database doesn't exist, this will create a new one! Wrap a `Arc<_>` around it to use it in parallel contexts!
18    fn open<P>(db: P) -> AtomicDatabase<Self>
19    where
20        P: AsRef<Path>,
21        Self: DeserializeOwned,
22    {
23        let db_path = db.as_ref();
24        if db_path.exists() {
25            AtomicDatabase::load(db_path).unwrap()
26        } else {
27            AtomicDatabase::create(db_path).unwrap()
28        }
29    }
30
31    /// Creates a Database instance in memory. Wrap a `Arc<_>` around it to use it in parallel contexts!
32    fn open_in_memory() -> AtomicDatabase<Self>
33    where
34        Self: DeserializeOwned,
35    {
36        AtomicDatabase::load_in_memory()
37    }
38
39    /// Loads file data into the `Database`
40    fn load(file: impl io::Read) -> std::io::Result<Self>
41    where
42        Self: Sized,
43        Self: DeserializeOwned,
44    {
45        Ok(serde_json::from_reader(file)?)
46    }
47
48    /// Saves data of the `Database` to a file
49    fn save(&self, file: impl io::Write) -> std::io::Result<()> {
50        let writer = BufWriter::new(file);
51        serde_json::to_writer_pretty(writer, self)?;
52        Ok(())
53    }
54}
55
56/// Synchronized Wrapper, that automatically saves changes when path and tmp are defined
57pub struct AtomicDatabase<T: DataStore> {
58    path: Option<PathBuf>,
59    /// Name of the DataStore temporary file
60    tmp: Option<PathBuf>,
61    data: RwLock<T>,
62}
63
64impl<T: DataStore + DeserializeOwned> AtomicDatabase<T> {
65    /// Load the database in memory.
66    pub fn load_in_memory() -> Self {
67        Self {
68            path: None,
69            tmp: None,
70            data: RwLock::new(T::default()),
71        }
72    }
73
74    /// Load the database from the file system.
75    pub fn load(path: &Path) -> Result<Self, std::io::Error> {
76        let new_path = path.with_extension("json");
77        let tmp = Self::tmp_path(&new_path)?;
78
79        let file = File::open(path)?;
80        // for the future: make here version checks
81        let data = T::load(file)?;
82        atomic_write(&tmp, &new_path, &data)?;
83
84        Ok(Self {
85            path: Some(new_path),
86            tmp: Some(tmp),
87            data: RwLock::new(data),
88        })
89    }
90
91    /// Create a new database and save it.
92    pub fn create(path: &Path) -> Result<Self, std::io::Error> {
93        let tmp = Self::tmp_path(path)?;
94
95        let data = Default::default();
96        atomic_write(&tmp, path, &data)?;
97
98        Ok(Self {
99            path: Some(path.into()),
100            tmp: Some(tmp),
101            data: RwLock::new(data),
102        })
103    }
104
105    /// Lock the database for reading.
106    pub fn read(&self) -> AtomicDatabaseRead<'_, T> {
107        AtomicDatabaseRead {
108            data: self.data.read().unwrap(),
109        }
110    }
111
112    /// Lock the database for writing. This will save the changes atomically on drop.
113    pub fn write(&self) -> AtomicDatabaseWrite<'_, T> {
114        AtomicDatabaseWrite {
115            path: self.path.as_deref(),
116            tmp: self.tmp.as_deref(),
117            data: self.data.write().unwrap(),
118        }
119    }
120
121    fn tmp_path(path: &Path) -> Result<PathBuf, std::io::Error> {
122        let mut tmp_name = OsString::from(".");
123        tmp_name.push(path.file_name().unwrap_or(OsStr::new("db")));
124        tmp_name.push("~");
125        let tmp = path.with_file_name(tmp_name);
126        if tmp.exists() {
127            error!(
128            "Found orphaned database temporary file '{tmp:?}'. The server has recently crashed or is already running. Delete this before continuing!"
129        );
130            return Err(std::io::Error::last_os_error());
131        }
132        Ok(tmp)
133    }
134}
135
136/// Atomic write routine, loosely inspired by the tempfile crate.
137///
138/// This assumes that the rename FS operations are atomic.
139fn atomic_write<T: DataStore>(tmp: &Path, path: &Path, data: &T) -> Result<(), std::io::Error> {
140    {
141        let mut tmpfile = File::create(tmp)?;
142        data.save(&mut tmpfile)?;
143        tmpfile.sync_all()?; // just to be sure!
144    }
145    fs::rename(tmp, path)?;
146    Ok(())
147}
148
149impl<T: DataStore> fmt::Debug for AtomicDatabase<T> {
150    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151        f.debug_struct("AtomicDatabase")
152            .field("file", &self.path)
153            .finish()
154    }
155}
156
157impl<T: DataStore> Drop for AtomicDatabase<T> {
158    fn drop(&mut self) {
159        if let Some(tmp) = &self.tmp {
160            if let Some(path) = &self.path {
161                info!("Saving database");
162                let guard = self.data.read().unwrap();
163                atomic_write(tmp, path, &*guard).unwrap();
164            }
165        }
166    }
167}
168
169pub struct AtomicDatabaseRead<'a, T: DataStore> {
170    data: RwLockReadGuard<'a, T>,
171}
172
173impl<'a, T: DataStore> Deref for AtomicDatabaseRead<'a, T> {
174    type Target = T;
175    fn deref(&self) -> &Self::Target {
176        &self.data
177    }
178}
179
180pub struct AtomicDatabaseWrite<'a, T: DataStore> {
181    tmp: Option<&'a Path>,
182    path: Option<&'a Path>,
183    data: RwLockWriteGuard<'a, T>,
184}
185
186impl<'a, T: DataStore> Deref for AtomicDatabaseWrite<'a, T> {
187    type Target = T;
188    fn deref(&self) -> &Self::Target {
189        &self.data
190    }
191}
192
193impl<'a, T: DataStore> DerefMut for AtomicDatabaseWrite<'a, T> {
194    fn deref_mut(&mut self) -> &mut Self::Target {
195        &mut self.data
196    }
197}
198
199impl<'a, T: DataStore> Drop for AtomicDatabaseWrite<'a, T> {
200    fn drop(&mut self) {
201        if let Some(tmp) = self.tmp {
202            if let Some(path) = self.path {
203                info!("Saving database");
204                atomic_write(tmp, path, &*self.data).unwrap();
205            }
206        }
207    }
208}