123use 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 _lockfile: lockfile::Lockfile,19}2021pub trait DbData: DeserializeOwned + Serialize + Default {22 const DB_NAME: &'static str;2324 fn open(db: &Db) -> Result<DbFile<Self>> {25 db.db::<Self>()26 }27}2829#[derive(Clone)]30pub struct Db(Arc<Mutex<DbInternal>>);31impl Db {32 pub fn new(root: impl AsRef<Path>) -> Result<Self> {33 let root: &Path = root.as_ref();34 std::fs::create_dir_all(&root).context("db root")?;35 let mut lockfile = root.to_owned();36 lockfile.push(".lock");37 let lockfile = lockfile::Lockfile::create(lockfile).context("db lock")?;38 Ok(Db(Arc::new(Mutex::new(DbInternal {39 root: root.to_owned(),40 locked_paths: HashSet::new(),41 _lockfile: lockfile,42 }))))43 }4445 pub fn db<T: DbData>(&self) -> Result<DbFile<T>> {46 let name = T::DB_NAME;47 assert!(!name.contains("/") && !name.contains("\\"));48 let mut db = self.0.lock().unwrap();49 let mut data_path = db.root.clone();50 data_path.push(format!("{}.toml", name));5152 if !db.locked_paths.insert(data_path.clone()) {53 anyhow::bail!("file is already open");54 }5556 let data = if data_path.exists() {57 let raw_data = std::fs::read(&data_path).context("reading file")?;58 toml::from_slice(&raw_data).context("parsing file")?59 } else {60 T::default()61 };6263 Ok(DbFile {64 db: self.clone(),65 root: db.root.clone(),66 path: data_path,67 data,68 dirty: Cell::new(false),69 })70 }71}7273pub struct DbFile<T: DbData> {74 db: Db,75 root: PathBuf,76 path: PathBuf,77 data: T,78 dirty: Cell<bool>,79}8081impl<T: DbData> Deref for DbFile<T> {82 type Target = T;8384 fn deref(&self) -> &Self::Target {85 &self.data86 }87}8889impl<T: DbData> DerefMut for DbFile<T> {90 fn deref_mut(&mut self) -> &mut Self::Target {91 self.dirty.set(true);92 &mut self.data93 }94}9596impl<T: DbData> DbFile<T> {97 pub fn write(&self) -> Result<()> {98 if !self.dirty.get() {99 return Ok(());100 }101 let mut temp = tempfile::Builder::new()102 .prefix("~")103 .suffix(".toml")104 .tempfile_in(&self.root)?;105 let mut out = String::new();106 let mut serializer = toml::Serializer::new(&mut out);107 serializer.pretty_array(true).pretty_string(true);108 self.data.serialize(&mut serializer)?;109 temp.write_all(&out.as_bytes())?;110 temp.persist(&self.path)?;111 self.dirty.set(false);112 Ok(())113 }114}115116impl<T: DbData> Drop for DbFile<T> {117 fn drop(&mut self) {118 let mut db = self.db.0.lock().unwrap();119 self.write().unwrap();120 db.locked_paths.remove(&self.path);121 }122}