use camino::{Utf8Path, Utf8PathBuf}; use serde::{Deserialize, Serialize}; use serde_json::Error as JsonError; use std::fmt; use std::io::Error as IOError; use crate::db::error::{BoxedError, DomainCreationError, UserCreationError}; use crate::db::{Database, DatabaseInterface, Domain, MemoryDatabase, User, UserRef}; #[derive(Debug)] pub enum FilesystemDatabaseError { ReadFileIO(Utf8PathBuf, IOError), ReadFileJson(Utf8PathBuf, JsonError), WriteFileIO(Utf8PathBuf, IOError), WriteFileJson(Utf8PathBuf, JsonError), } impl fmt::Display for FilesystemDatabaseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "{}", match self { Self::ReadFileIO(path, e) => format!("Failed to read database file {path}: {e}"), Self::ReadFileJson(path, e) => format!("Failed to parse database JSON file {path}: {e}"), Self::WriteFileIO(path, e) => format!("Failed to write database file {path}: {e}"), Self::WriteFileJson(path, e) => format!("Failed to convert database to JSON file {path}: {e}"), } ) } } impl std::error::Error for FilesystemDatabaseError {} #[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct FilesystemDatabase { // TODO: Once we have common validation steps in place across DB backends, we can reduce cloning. // For now, we clone the DB on every write operation, and update it when saving to disk // succeeds. pub inner: MemoryDatabase, pub path: Utf8PathBuf, } impl FilesystemDatabase { pub async fn from_path(path: impl AsRef) -> Result, BoxedError> { let path = path.as_ref(); let s = tokio::fs::read(path) .await .map_err(|e| Box::new(FilesystemDatabaseError::ReadFileIO(path.to_path_buf(), e)))?; let inner: MemoryDatabase = serde_json::from_slice(&s) .map_err(|e| Box::new(FilesystemDatabaseError::ReadFileJson(path.to_path_buf(), e)))?; Ok(Database::new(Self { inner, path: path.to_path_buf(), })) } pub async fn save(&mut self) -> Result<(), BoxedError> { let s = serde_json::to_string(&self) .map_err(|e| Box::new(FilesystemDatabaseError::WriteFileJson(self.path.clone(), e)))?; Ok(tokio::fs::write(&self.path, &s) .await .map_err(|e| Box::new(FilesystemDatabaseError::WriteFileIO(self.path.clone(), e)))?) } } impl DatabaseInterface for FilesystemDatabase { async fn create_domain( &mut self, domain: &str, ) -> Result, BoxedError> { let mut new_db = self.inner.clone(); if let Err(e) = new_db.create_domain(domain).await? { return Ok(Err(e)); } self.save().await?; self.inner = new_db; Ok(Ok(())) } fn get_domain(&self, domain: &str) -> impl Future, BoxedError>> { self.inner.get_domain(domain) } fn get_user( &self, req_user: &UserRef, ) -> impl Future, BoxedError>> { self.inner.get_user(req_user) } async fn create_user( &mut self, user: User, ) -> Result, BoxedError> { let mut new_db = self.inner.clone(); if let Err(e) = new_db.create_user(user).await? { return Ok(Err(e)); } self.save().await?; self.inner = new_db; Ok(Ok(())) } async fn try_create_user( &mut self, new_user: User, current_user: &User, ) -> Result, BoxedError> { let mut new_db = self.inner.clone(); if let Err(e) = new_db.try_create_user(new_user, current_user).await? { return Ok(Err(e)); } self.save().await?; self.inner = new_db; Ok(Ok(())) } fn list_all_domains(&self) -> impl Future, BoxedError>> { self.inner.list_all_domains() } fn list_all_users(&self) -> impl Future, BoxedError>> { self.inner.list_all_users() } fn list_domain_users( &self, domain: Option, ) -> impl Future, BoxedError>> { self.inner.list_domain_users(domain) } }