hyperlog/crates/hyperlog-tui/src/storage.rs
kjuulh 4a0fcd1bbb
All checks were successful
continuous-integration/drone/push Build is passing
feat: move core to tui and begin grpc work
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-11 23:23:00 +02:00

262 lines
6.3 KiB
Rust

use std::{
path::{Path, PathBuf},
sync::{Arc, Mutex},
time::Duration,
};
use crate::{engine::Engine, shared_engine::SharedEngine};
pub struct LockFile(PathBuf);
impl Drop for LockFile {
fn drop(&mut self) {
tracing::debug!("removing lockfile");
std::fs::remove_file(&self.0).expect("to be able to delete lockfile")
}
}
impl From<PathBuf> for LockFile {
fn from(value: PathBuf) -> Self {
Self(value)
}
}
#[derive(Clone)]
pub struct Storage {
base: PathBuf,
lock_file: Arc<Mutex<Option<LockFile>>>,
}
impl Default for Storage {
fn default() -> Self {
Self::new()
}
}
impl Storage {
pub fn new() -> Self {
let data_dir = dirs::data_local_dir()
.ok_or(anyhow::anyhow!("failed to retrieve the users data dir"))
.expect("to be able to find config");
Self {
base: data_dir,
lock_file: Arc::new(Mutex::new(None)),
}
}
pub fn with_base(&mut self, base: &Path) {
self.base = base.to_path_buf();
}
pub fn store(&self, engine: &SharedEngine) -> anyhow::Result<()> {
let state_path = self.state()?;
std::fs::write(state_path, engine.to_str()?)?;
Ok(())
}
pub fn load(&self) -> anyhow::Result<Engine> {
let mut lock = self.lock_file.lock().unwrap();
if lock.is_none() {
let lock_file = self.state_lock_file()?;
*lock = Some(lock_file);
}
let engine = match self.state_file()? {
Some(contents) => Engine::engine_from_str(&contents)?,
None => Engine::default(),
};
Ok(engine)
}
pub fn unload(self) -> anyhow::Result<()> {
drop(self);
Ok(())
}
pub fn clear_lock_file(self) {
let mut lock_file = self.lock_file.lock().unwrap();
if lock_file.is_some() {
*lock_file = None;
}
}
fn state(&self) -> anyhow::Result<PathBuf> {
self.cache().map(|c| c.join("graph.json"))
}
fn state_file(&self) -> anyhow::Result<Option<String>> {
let state_path = self.state()?;
if !state_path.exists() {
return Ok(None);
}
let contents = std::fs::read_to_string(&state_path)?;
Ok(Some(contents))
}
fn state_lock(&self) -> anyhow::Result<PathBuf> {
self.cache().map(|c| c.join("graph.lock"))
}
fn create_lock_file(&self) -> anyhow::Result<()> {
let lock_path = self.state_lock()?;
if let Some(parent) = lock_path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(lock_path, "hyperlog-lock")?;
Ok(())
}
fn state_lock_file(&self) -> anyhow::Result<LockFile> {
let lock_path = self.state_lock()?;
if !lock_path.exists() {
self.create_lock_file()?;
return Ok(LockFile::from(lock_path));
}
if let Ok(modified) = lock_path.metadata()?.modified() {
if modified.elapsed()? > Duration::from_secs(86400) {
std::fs::remove_file(&lock_path)?;
self.create_lock_file()?;
return Ok(LockFile::from(lock_path));
}
}
anyhow::bail!("lock file exists and is valid. Aborting");
}
fn cache(&self) -> anyhow::Result<PathBuf> {
Ok(self.base.join("hyperlog"))
}
pub fn info(&self) -> anyhow::Result<String> {
Ok(format!("storage:\n\tgraph: {}", self.state()?.display()))
}
}
#[cfg(test)]
mod test {
use std::collections::BTreeMap;
use hyperlog_core::log::GraphItem;
use similar_asserts::assert_eq;
use super::*;
#[test]
fn can_create_state() -> anyhow::Result<()> {
let tempdir = tempfile::tempdir()?;
let mut storage = Storage::default();
storage.with_base(tempdir.path());
let engine = SharedEngine::from(storage.load()?);
engine.create_root("can_create_state")?;
storage.store(&engine)?;
let graph = std::fs::read_to_string(tempdir.path().join("hyperlog").join("graph.json"))?;
let lock = std::fs::read_to_string(tempdir.path().join("hyperlog").join("graph.lock"))?;
assert_eq!(
r#"{
"can_create_state": {
"type": "user"
}
}"#
.to_string(),
graph
);
assert_eq!(r#"hyperlog-lock"#.to_string(), lock);
Ok(())
}
#[test]
fn lock_already_exists() -> anyhow::Result<()> {
let tempdir = tempfile::tempdir()?;
let mut storage = Storage::default();
storage.with_base(tempdir.path());
let _engine = storage.load()?;
let mut storage_should_fail = Storage::default();
storage_should_fail.with_base(tempdir.path());
let engine_should_fail = storage_should_fail.load();
assert!(engine_should_fail.is_err());
if let Err(e) = engine_should_fail {
assert_eq!(
"lock file exists and is valid. Aborting".to_string(),
e.to_string()
);
}
Ok(())
}
#[test]
fn lock_is_cleaned_up() -> anyhow::Result<()> {
let tempdir = tempfile::tempdir()?;
let mut storage = Storage::default();
storage.with_base(tempdir.path());
let engine = SharedEngine::from(storage.load()?);
engine.create_root("can_create_state")?;
storage.store(&engine)?;
storage.unload()?;
assert!(!tempdir.path().join("hyperlog").join("graph.lock").exists());
Ok(())
}
#[test]
fn can_load_state() -> anyhow::Result<()> {
let tempdir = tempfile::tempdir()?;
let mut storage = Storage::default();
storage.with_base(tempdir.path());
let engine = SharedEngine::from(storage.load()?);
engine.create_root("can_create_state")?;
storage.store(&engine)?;
let graph = std::fs::read_to_string(tempdir.path().join("hyperlog").join("graph.json"))?;
assert_eq!(
r#"{
"can_create_state": {
"type": "user"
}
}"#
.to_string(),
graph
);
let engine = storage.load()?;
let res = engine.get("can_create_state", &[]);
assert_eq!(Some(GraphItem::User(BTreeMap::default())), res.cloned());
Ok(())
}
}