git.delta.rocks / jrsonnet / refs/commits / fad91f8a3923

difftreelog

source

src/db/db.rs2.5 KiBsourcehistory
1//! Small .toml based readable data store23use anyhow::{Context, Result};4use serde::{de::DeserializeOwned, Serialize};5use std::{6	cell::Cell,7	collections::HashSet,8	io::Write,9	ops::{Deref, DerefMut},10	path::Path,11	path::PathBuf,12	sync::{Arc, Mutex},13};1415struct DbInternal {16	root: PathBuf,17	locked_paths: HashSet<PathBuf>,18}1920pub trait DbData: DeserializeOwned + Serialize + Default {21	const DB_NAME: &'static str;2223	fn open(db: &Db) -> Result<DbFile<Self>> {24		db.db::<Self>()25	}26}2728#[derive(Clone)]29pub struct Db(Arc<Mutex<DbInternal>>);30impl Db {31	pub fn new(root: impl AsRef<Path>) -> Result<Self> {32		let root: &Path = root.as_ref();33		std::fs::create_dir_all(&root).context("db root")?;34		Ok(Db(Arc::new(Mutex::new(DbInternal {35			root: root.to_owned(),36			locked_paths: HashSet::new(),37		}))))38	}3940	pub fn db<T: DbData>(&self) -> Result<DbFile<T>> {41		let name = T::DB_NAME;42		assert!(!name.contains("/") && !name.contains("\\"));43		let mut db = self.0.lock().unwrap();44		let mut data_path = db.root.clone();45		data_path.push(format!("{}.toml", name));4647		if !db.locked_paths.insert(data_path.clone()) {48			anyhow::bail!("file is already open");49		}5051		let data = if data_path.exists() {52			let raw_data = std::fs::read(&data_path).context("reading file")?;53			toml::from_slice(&raw_data).context("parsing file")?54		} else {55			T::default()56		};5758		Ok(DbFile {59			db: self.clone(),60			root: db.root.clone(),61			path: data_path,62			data,63			dirty: Cell::new(false),64		})65	}66}6768pub struct DbFile<T: DbData> {69	db: Db,70	root: PathBuf,71	path: PathBuf,72	data: T,73	dirty: Cell<bool>,74}7576impl<T: DbData> Deref for DbFile<T> {77	type Target = T;7879	fn deref(&self) -> &Self::Target {80		&self.data81	}82}8384impl<T: DbData> DerefMut for DbFile<T> {85	fn deref_mut(&mut self) -> &mut Self::Target {86		self.dirty.set(true);87		&mut self.data88	}89}9091impl<T: DbData> DbFile<T> {92	pub fn write(&self) -> Result<()> {93		if !self.dirty.get() {94			return Ok(());95		}96		let mut temp = tempfile::Builder::new()97			.prefix("~")98			.suffix(".toml")99			.tempfile_in(&self.root)?;100		let mut out = String::new();101		let mut serializer = toml::Serializer::new(&mut out);102		serializer.pretty_array(true).pretty_string(true);103		self.data.serialize(&mut serializer)?;104		temp.write_all(&out.as_bytes())?;105		temp.persist(&self.path)?;106		self.dirty.set(false);107		Ok(())108	}109}110111impl<T: DbData> Drop for DbFile<T> {112	fn drop(&mut self) {113		let mut db = self.db.0.lock().unwrap();114		self.write().unwrap();115		db.locked_paths.remove(&self.path);116	}117}