light_magic/
atomic.rs

1use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};
2use serde::{de::DeserializeOwned, Serialize};
3use std::{
4    ffi::{OsStr, OsString},
5    fmt,
6    fs::{self, File},
7    io::{self},
8    ops::{Deref, DerefMut},
9    path::{Path, PathBuf},
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 JSON file.
49    fn save(&self, mut file: impl io::Write) -> std::io::Result<()> {
50        serde_json::to_writer_pretty(&mut file, self)?;
51        Ok(())
52    }
53}
54
55/// Synchronized Wrapper, that automatically saves changes when path and tmp are defined.
56pub struct AtomicDatabase<T: DataStore> {
57    path: Option<PathBuf>,
58    /// Name of the DataStore temporary file.
59    tmp: Option<PathBuf>,
60    data: RwLock<T>,
61}
62
63impl<T: DataStore + DeserializeOwned> AtomicDatabase<T> {
64    /// Loads the database in memory.
65    pub fn load_in_memory() -> Self {
66        Self {
67            path: None,
68            tmp: None,
69            data: RwLock::new(T::default()),
70        }
71    }
72
73    /// Loads the database from the file system.
74    pub fn load(path: &Path) -> Result<Self, std::io::Error> {
75        let tmp = Self::tmp_path(path)?;
76        let file = File::open(path)?;
77        // for the future: make here version checks
78        let data = T::load(file)?;
79        atomic_write(&tmp, path, &data)?;
80
81        Ok(Self {
82            path: Some(path.into()),
83            tmp: Some(tmp),
84            data: RwLock::new(data),
85        })
86    }
87
88    /// Creates a new database and save it.
89    pub fn create(path: &Path) -> Result<Self, std::io::Error> {
90        let tmp = Self::tmp_path(path)?;
91
92        let data = Default::default();
93        atomic_write(&tmp, path, &data)?;
94
95        Ok(Self {
96            path: Some(path.into()),
97            tmp: Some(tmp),
98            data: RwLock::new(data),
99        })
100    }
101
102    /// Locks the database for reading.
103    pub fn read(&self) -> AtomicDatabaseRead<'_, T> {
104        AtomicDatabaseRead {
105            data: self.data.read(),
106        }
107    }
108
109    /// Locks the database for writing. This will save the changes atomically on drop.
110    pub fn write(&self) -> AtomicDatabaseWrite<'_, T> {
111        AtomicDatabaseWrite {
112            path: self.path.as_deref(),
113            tmp: self.tmp.as_deref(),
114            data: self.data.write(),
115        }
116    }
117
118    fn tmp_path(path: &Path) -> Result<PathBuf, std::io::Error> {
119        let mut tmp_name = OsString::from(".");
120        tmp_name.push(path.file_name().unwrap_or(OsStr::new("db")));
121        tmp_name.push("~");
122        let tmp = path.with_file_name(tmp_name);
123        if tmp.exists() {
124            error!(
125                "Found orphaned database temporary file '{tmp:?}'. \
126                 The server has recently crashed or is already running. \
127                 Delete this before continuing!"
128            );
129            return Err(std::io::Error::new(
130                std::io::ErrorKind::AlreadyExists,
131                "orphaned temporary file exists",
132            ));
133        }
134        Ok(tmp)
135    }
136}
137
138/// Atomic write routine, loosely inspired by the tempfile crate.
139///
140/// This assumes that the rename FS operation is atomic.
141fn atomic_write<T: DataStore>(tmp: &Path, path: &Path, data: &T) -> Result<(), std::io::Error> {
142    {
143        let mut tmpfile = File::create(tmp)?;
144        data.save(&mut tmpfile)?;
145        tmpfile.sync_all()?; // just to be sure!
146    }
147    fs::rename(tmp, path)?;
148    Ok(())
149}
150
151impl<T: DataStore> fmt::Debug for AtomicDatabase<T> {
152    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153        f.debug_struct("AtomicDatabase")
154            .field("file", &self.path)
155            .finish()
156    }
157}
158
159impl<T: DataStore> Drop for AtomicDatabase<T> {
160    fn drop(&mut self) {
161        if let (Some(tmp), Some(path)) = (&self.tmp, &self.path) {
162            info!("Saving database");
163            let guard = self.data.read();
164            if let Err(e) = atomic_write(tmp, path, &*guard) {
165                error!("Failed to save database on drop: {}", e);
166            }
167        }
168    }
169}
170
171pub struct AtomicDatabaseRead<'a, T: DataStore> {
172    data: RwLockReadGuard<'a, T>,
173}
174
175impl<'a, T: DataStore> Deref for AtomicDatabaseRead<'a, T> {
176    type Target = T;
177    fn deref(&self) -> &Self::Target {
178        &self.data
179    }
180}
181
182pub struct AtomicDatabaseWrite<'a, T: DataStore> {
183    tmp: Option<&'a Path>,
184    path: Option<&'a Path>,
185    data: RwLockWriteGuard<'a, T>,
186}
187
188impl<'a, T: DataStore> Deref for AtomicDatabaseWrite<'a, T> {
189    type Target = T;
190    fn deref(&self) -> &Self::Target {
191        &self.data
192    }
193}
194
195impl<'a, T: DataStore> DerefMut for AtomicDatabaseWrite<'a, T> {
196    fn deref_mut(&mut self) -> &mut Self::Target {
197        &mut self.data
198    }
199}
200
201impl<'a, T: DataStore> Drop for AtomicDatabaseWrite<'a, T> {
202    fn drop(&mut self) {
203        if let (Some(tmp), Some(path)) = (self.tmp, self.path) {
204            info!("Saving database");
205            if let Err(e) = atomic_write(tmp, path, &*self.data) {
206                error!("Failed to save database: {}", e);
207            }
208        }
209    }
210}