use fd_lock::FdLock; use serde::{Deserialize, Serialize}; use sha2::{Sha256, Digest}; use std::fs; use std::fs::File; use crate::conf; use crate::error; #[cfg(test)] pub const PATH: &str = "clinte.json"; #[cfg(not(test))] pub const PATH: &str = "/usr/local/clinte/clinte.json"; #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Post { pub title: String, pub author: String, pub body: String, } #[derive(Debug, Deserialize, Serialize)] pub struct Posts { posts: Vec, } #[derive(Debug)] pub struct Conn { pub conn: FdLock, } impl Conn { pub fn init(path: &str) -> Self { if *conf::DEBUG { log::info!("Opening clinte.json"); } let file = error::helper(File::open(path), "Couldn't open clinte.json"); Self { conn: FdLock::new(file), } } } impl Posts { pub fn get_all(path: &str) -> Self { if *conf::DEBUG { log::info!("Retrieving posts..."); } let mut db = Conn::init(path); let _guard = error::helper(db.conn.try_lock(), "Couldn't acquire lock on clinte.json"); let strdata = error::helper(fs::read_to_string(PATH), "Couldn't read clinte.json"); let out: Self = error::helper(serde_json::from_str(&strdata), "Couldn't parse clinte.json"); out } // Hash the contents of clinte.json and store it in your // home directory. Useful for determining if there are // new posts. pub fn hash(&self) -> error::Result<()> { let body = error::helper(serde_json::to_string_pretty(&self), "Couldn't reserialize json to record hash"); let mut hasher = Sha256::new(); hasher.update(body); let hash = format!("{:x}", hasher.finalize()); #[cfg(test)] let homedir = std::env::var("PWD")?; #[cfg(not(test))] let homedir = std::env::var("HOME")?; let writepath = format!("{}/.clinte.sha256", homedir); fs::write(writepath, &hash)?; Ok(()) } pub fn replace(&mut self, n: usize, post: Post) { self.posts[n] = post; } pub fn get(&self, n: usize) -> Post { self.posts[n].clone() } pub fn append(&mut self, post: Post) { self.posts.push(post); } pub fn delete(&mut self, n: usize) { self.posts.remove(n); } pub fn write(&self) { let strdata = error::helper( serde_json::to_string_pretty(&self), "Couldn't serialize posts", ); let mut db_fd = Conn::init(PATH); let _guard = error::helper( db_fd.conn.try_lock(), "Couldn't acquire lock on clinte.json", ); error::helper( fs::write(PATH, &strdata), "Couldn't write data to clinte.json", ); } pub fn posts(&self) -> Vec { self.posts.clone() } } #[cfg(test)] mod tests { use super::*; use crate::user; #[test] fn retrieve_posts_and_crud() { let mut all = Posts::get_all(PATH); assert_eq!(all.posts.len(), 1); let post = all.get(0); assert_eq!(post.title, "Welcome to CLI NoTEs!"); assert_eq!(post.author, "clinte!"); assert_eq!(post.body, "Welcome to clinte! For usage, run 'clinte -h'"); let user = &*user::NAME; all.append(Post { author: user.into(), title: String::from("TITLE_HERE"), body: String::from("BODY_HERE"), }); all.write(); let mut all = Posts::get_all(PATH); let post = all.get(1); assert_eq!(post.title, "TITLE_HERE"); assert_eq!(post.author, *user); assert_eq!(post.body, "BODY_HERE"); let post = Post { author: user.into(), title: "TITLE_GOES_HERE".into(), body: "BODY_GOES_HERE".into(), }; all.replace(1, post); all.write(); let mut all = Posts::get_all(PATH); let post = all.get(1); assert_eq!(post.title, "TITLE_GOES_HERE"); assert_eq!(post.author, *user); assert_eq!(post.body, "BODY_GOES_HERE"); all.delete(1); all.write(); } #[test] fn check_hash() { let strposts = fs::read_to_string(PATH).unwrap(); let mut hasher = Sha256::new(); hasher.update(strposts); let rhs = format!("{:x}", hasher.finalize()); let all = Posts::get_all(PATH); all.hash().unwrap(); let pwd = std::env::var("PWD").unwrap(); let hashpath = format!("{}/.clinte.sha256", pwd); let lhs = fs::read_to_string(hashpath.clone()).unwrap(); fs::remove_file(hashpath).unwrap(); assert_eq!(lhs, rhs); } }