Skip to main content

yog_storage/
shelf.rs

1//! The stores open in this process: one each, however many handles; read
2//! from their file once, written back when asked ([`flush_all`], a store's
3//! `flush`) — atomically, so a crash mid-save leaves the old one.
4
5use std::collections::HashMap;
6use std::io::{self, Write};
7use std::path::{Path, PathBuf};
8
9use yog_sync::{Arc, Mutex};
10
11/// One store's values, and whether they changed since they were written.
12#[derive(Default)]
13pub(crate) struct Shelf {
14    /// Its file; none for a session's (never written).
15    pub(crate) path: Option<PathBuf>,
16    pub(crate) values: yog_data::Map,
17    pub(crate) dirty: bool,
18}
19
20/// Which store: a file's, or a mod's session.
21#[derive(Clone, PartialEq, Eq, Hash)]
22enum Key {
23    File(PathBuf),
24    Session(String),
25}
26
27type Shelves = HashMap<Key, Arc<Mutex<Shelf>>>;
28
29static SHELVES: Mutex<Option<Shelves>> = Mutex::new(None);
30
31/// The store in the file at `path`, read the first time.
32pub(crate) fn file(path: PathBuf) -> Arc<Mutex<Shelf>> {
33    open(Key::File(path.clone()), || Shelf {
34        values: read(&path),
35        path: Some(path),
36        dirty: false,
37    })
38}
39
40/// `mod_id`'s session store.
41pub(crate) fn session(mod_id: &str) -> Arc<Mutex<Shelf>> {
42    open(Key::Session(mod_id.to_owned()), Shelf::default)
43}
44
45fn open(key: Key, new: impl FnOnce() -> Shelf) -> Arc<Mutex<Shelf>> {
46    let mut shelves = SHELVES.lock();
47    let shelves = shelves.get_or_insert_with(HashMap::new);
48    shelves
49        .entry(key)
50        .or_insert_with(|| Arc::new(Mutex::new(new())))
51        .clone()
52}
53
54/// Writes every store changed since it was last written; the first failure.
55pub fn flush_all() -> io::Result<()> {
56    let open: Vec<_> = SHELVES
57        .lock()
58        .iter()
59        .flat_map(|s| s.values().cloned())
60        .collect();
61    let mut result = Ok(());
62    for shelf in open {
63        if let Err(e) = shelf.lock().flush()
64            && result.is_ok()
65        {
66            result = Err(e);
67        }
68    }
69    result
70}
71
72/// Writes and closes every store under `root` (the world closed: the next
73/// one opened reads its own).
74pub fn close_under(root: &Path) -> io::Result<()> {
75    let closing: Vec<_> = {
76        let mut shelves = SHELVES.lock();
77        let Some(shelves) = shelves.as_mut() else {
78            return Ok(());
79        };
80        let keys: Vec<Key> = shelves
81            .keys()
82            .filter(|k| matches!(k, Key::File(p) if p.starts_with(root)))
83            .cloned()
84            .collect();
85        keys.iter().filter_map(|k| shelves.remove(k)).collect()
86    };
87    let mut result = Ok(());
88    for shelf in closing {
89        if let Err(e) = shelf.lock().flush()
90            && result.is_ok()
91        {
92            result = Err(e);
93        }
94    }
95    result
96}
97
98impl Shelf {
99    /// Writes it if it changed since it was last written (a session's never is).
100    pub(crate) fn flush(&mut self) -> io::Result<()> {
101        let Some(path) = &self.path else {
102            return Ok(());
103        };
104        if !self.dirty {
105            return Ok(());
106        }
107        write(path, &self.values)?;
108        self.dirty = false;
109        Ok(())
110    }
111}
112
113/// What a store's written to before it's renamed over the old one.
114const TMP_EXT: &str = yog_ids::concatcp!(yog_ids::Format::Data.ext(), yog_ids::sep::DOT, "tmp");
115
116fn write(path: &Path, values: &yog_data::Map) -> io::Result<()> {
117    if let Some(parent) = path.parent() {
118        std::fs::create_dir_all(parent)?;
119    }
120    let tmp = path.with_extension(TMP_EXT);
121    {
122        let mut f = std::fs::File::create(&tmp)?;
123        f.write_all(&yog_data::binary::encode(&values.clone().into()))?;
124        f.flush()?;
125    }
126    std::fs::rename(&tmp, path)
127}
128
129/// The values in the file at `path`: none when there's no such file or it
130/// isn't a store.
131fn read(path: &Path) -> yog_data::Map {
132    let Ok(bytes) = std::fs::read(path) else {
133        return yog_data::Map::new();
134    };
135    match yog_data::binary::decode(&bytes) {
136        Ok(yog_data::Value::Map(m)) => m,
137        _ => yog_data::Map::new(),
138    }
139}