Skip to main content

slowshell_core/
persistence.rs

1use serde::{Serialize, de::DeserializeOwned};
2use std::{
3  env, fs,
4  path::{Path, PathBuf},
5};
6
7pub fn persistence_location() -> PathBuf {
8  env::var_os("XDG_STATE_HOME")
9    .map(PathBuf::from)
10    .unwrap_or_else(|| PathBuf::from(env::var_os("HOME").expect("HOME not set")).join(".local"))
11    .join("slowshell")
12}
13
14fn file_for(name: &str) -> Option<PathBuf> {
15  if name.is_empty() || name.contains('/') || name.contains('\\') || name == "." || name == ".." {
16    return None;
17  }
18  Some(persistence_location().join(format!("{name}.ron")))
19}
20
21pub fn load<T: DeserializeOwned>(name: impl AsRef<str>) -> Option<T> {
22  let name = name.as_ref();
23  let path = file_for(name)?;
24  let content = fs::read_to_string(path).ok()?;
25  match ron::from_str(&content) {
26    Ok(value) => Some(value),
27    Err(e) => {
28      eprintln!("[persistence] failed to load {name:?}: {e}");
29      None
30    }
31  }
32}
33
34use miette::IntoDiagnostic;
35
36pub fn save<T: Serialize>(name: impl AsRef<str>, value: &T) -> miette::Result<()> {
37  let name = name.as_ref();
38  let path =
39    file_for(name).ok_or_else(|| miette::miette!("[persistence] invalid name {name:?}"))?;
40  let dir = path
41    .parent()
42    .ok_or_else(|| miette::miette!("invalid path"))?;
43  fs::create_dir_all(dir).into_diagnostic()?;
44
45  let content = ron::ser::to_string_pretty(value, ron::ser::PrettyConfig::default())
46    .map_err(|e| miette::miette!("[persistence] failed to serialize {name:?}: {e}"))?;
47
48  atomic_write(&path, content.as_bytes())?;
49  Ok(())
50}
51
52pub fn has(name: impl AsRef<str>) -> bool {
53  file_for(name.as_ref()).is_some_and(|p| p.exists())
54}
55
56pub fn remove(name: impl AsRef<str>) -> miette::Result<()> {
57  let name = name.as_ref();
58  let path =
59    file_for(name).ok_or_else(|| miette::miette!("[persistence] invalid name {name:?}"))?;
60  if path.exists() {
61    fs::remove_file(&path).into_diagnostic()?;
62  }
63  Ok(())
64}
65
66fn atomic_write(path: &Path, content: &[u8]) -> miette::Result<()> {
67  let mut tmp = path.as_os_str().to_owned();
68  tmp.push(format!(".{}.tmp", std::process::id()));
69  let tmp = PathBuf::from(tmp);
70
71  let result = fs::write(&tmp, content)
72    .into_diagnostic()
73    .and_then(|()| fs::rename(&tmp, path).into_diagnostic());
74
75  if result.is_err() {
76    let _ = fs::remove_file(&tmp);
77  }
78
79  result
80}