93 lines
3.3 KiB
Rust
93 lines
3.3 KiB
Rust
//! Versioned settings files, shared and per app (§18.1).
|
|
|
|
use std::{
|
|
io::{self, Write as _},
|
|
path::{Path, PathBuf},
|
|
};
|
|
|
|
use serde_json::{Map, Value};
|
|
|
|
use crate::{AppId, lock::FileLock};
|
|
|
|
mod shared;
|
|
pub use shared::{APPEARANCE, AUTHOR, Appearance, CHECK_UPDATES, LANGUAGE, Language, MOTION, Motion, OMARCHY_SYNC, Shared, word};
|
|
|
|
/// The layout this build reads and writes.
|
|
pub const VERSION: u64 = 1;
|
|
|
|
/// One settings file; writes keep unknown fields.
|
|
#[derive(Clone, Debug)]
|
|
pub struct Store {
|
|
path: PathBuf,
|
|
}
|
|
|
|
impl Store {
|
|
/// Theme, language and motion, read by every app.
|
|
pub fn shared() -> io::Result<Self> {
|
|
let root = crate::dirs::root().map_err(io::Error::other)?;
|
|
Ok(Self::at(root.join("settings.json")))
|
|
}
|
|
|
|
/// One app's own preferences.
|
|
pub fn app(app: AppId) -> io::Result<Self> {
|
|
Ok(Self::at(crate::data_dir(app).map_err(io::Error::other)?.join("settings.json")))
|
|
}
|
|
|
|
pub fn at(path: PathBuf) -> Self {
|
|
Self { path }
|
|
}
|
|
|
|
pub fn path(&self) -> &Path {
|
|
&self.path
|
|
}
|
|
|
|
/// The stored fields; none when the file is absent.
|
|
pub fn read(&self) -> io::Result<Map<String, Value>> {
|
|
let bytes = match std::fs::read(&self.path) {
|
|
Ok(bytes) => bytes,
|
|
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(Map::new()),
|
|
Err(error) => return Err(error),
|
|
};
|
|
let invalid = |why: String| io::Error::new(io::ErrorKind::InvalidData, format!("{}: {why}", self.path.display()));
|
|
let Value::Object(fields) = serde_json::from_slice(&bytes).map_err(|error| invalid(error.to_string()))? else {
|
|
return Err(invalid("not a JSON object".into()));
|
|
};
|
|
match fields.get("version") {
|
|
None => Ok(fields),
|
|
Some(Value::Number(n)) if n.as_u64().is_some_and(|n| n <= VERSION) => Ok(fields),
|
|
Some(Value::Number(n)) => Err(invalid(format!("version {n} is newer than this Ely reads"))),
|
|
Some(_) => Err(invalid("the version is not a number".into())),
|
|
}
|
|
}
|
|
|
|
/// Sets `key` under the lock; other fields stay.
|
|
pub fn set(&self, key: &str, value: Value) -> io::Result<()> {
|
|
self.set_if(key, value, || true).map(|_| ())
|
|
}
|
|
|
|
/// Sets `key` if `still` holds once locked.
|
|
pub fn set_if(&self, key: &str, value: Value, still: impl FnOnce() -> bool) -> io::Result<bool> {
|
|
let dir = self.path.parent().ok_or_else(|| io::Error::other("settings have no folder"))?;
|
|
std::fs::create_dir_all(dir)?;
|
|
let _held = FileLock::take(&dir.join(".settings.lock"))?;
|
|
// A newer choice made while we waited wins.
|
|
if !still() {
|
|
log::info!("settings: {key} overtaken while waiting");
|
|
return Ok(false);
|
|
}
|
|
// Read under the lock: another app may just have written.
|
|
let mut fields = self.read()?;
|
|
fields.insert(key.into(), value);
|
|
fields.insert("version".into(), VERSION.into());
|
|
let mut temp = tempfile::NamedTempFile::new_in(dir)?;
|
|
temp.write_all(&serde_json::to_vec_pretty(&Value::Object(fields))?)?;
|
|
temp.as_file().sync_all()?;
|
|
temp.persist(&self.path).map_err(|error| error.error)?;
|
|
log::info!("settings: {key} saved in {}", self.path.display());
|
|
Ok(true)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|