about summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
authorBen Morrison <ben@gbmor.dev>2020-05-28 02:58:57 -0400
committerBen Morrison <ben@gbmor.dev>2020-05-28 02:58:57 -0400
commitbb327d381e5626d96942a805e6a5f4d4a5a771d5 (patch)
treec7e86c2a6ea8dc67be2221e8517806f365aac088 /src
parente4af0011c04e2861e4f890bcb7383f9c87e35126 (diff)
downloadclinte-bb327d381e5626d96942a805e6a5f4d4a5a771d5.tar.gz
extensive rewrite to use json for storage:
sqlite3 requires the directory where the database resides to be
writeable by the user. This presents a problem on multiuser UNIX systems
where they may want to limit areas where users have write access.

This rewrite totally scraps the sqlite3 database in favor of a pretty
simple json file consisting of an array of posts. flock(2) locking is
used to synchronize access to the file and make sure two clients aren't
trying to write to it at once. The locking is fairly granular right now,
but later I may change it to using a single lock for the duration of
execution since race conditions are *possible*, if unlikely for the
purposes of clinte's intended use.
Diffstat (limited to 'src')
-rw-r--r--src/db.rs121
-rw-r--r--src/main.rs14
-rw-r--r--src/posts.rs181
3 files changed, 140 insertions, 176 deletions
diff --git a/src/db.rs b/src/db.rs
index 84f209b..90ff548 100644
--- a/src/db.rs
+++ b/src/db.rs
@@ -1,68 +1,94 @@
-use std::time;
+use fd_lock::FdLock;
+use serde::{Deserialize, Serialize};
+
+use std::fs;
+use std::fs::File;
 
 use crate::conf;
 use crate::error;
 
-const DB_PATH: &str = "/usr/local/clinte/clinte.db";
+#[cfg(test)]
+pub const PATH: &str = "clinte.json";
+
+#[cfg(not(test))]
+pub const PATH: &str = "/usr/local/clinte/clinte.json";
 
-#[derive(Debug)]
+#[derive(Debug, Deserialize, Serialize)]
 pub struct Post {
-    pub id: u32,
     pub title: String,
     pub author: String,
     pub body: String,
 }
 
+#[derive(Debug, Deserialize, Serialize)]
+pub struct Posts {
+    pub posts: Vec<Post>,
+}
+
 #[derive(Debug)]
 pub struct Conn {
-    pub conn: rusqlite::Connection,
+    pub conn: FdLock<std::fs::File>,
 }
 
 impl Conn {
-    pub fn init(path: &str) -> rusqlite::Connection {
-        let start = time::Instant::now();
-
+    pub fn init(path: &str) -> Self {
         if *conf::DEBUG {
-            log::info!("Connecting to database");
+            log::info!("Opening clinte.json");
         }
 
-        let conn = error::helper(
-            rusqlite::Connection::open_with_flags(
-                path,
-                rusqlite::OpenFlags::SQLITE_OPEN_FULL_MUTEX
-                    | rusqlite::OpenFlags::SQLITE_OPEN_CREATE
-                    | rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE,
-            ),
-            "Could not connect to DB",
-        );
+        let file = error::helper(File::open(path), "Couldn't open clinte.json");
 
-        error::helper(
-            conn.execute(
-                "CREATE TABLE IF NOT EXISTS posts (
-            id INTEGER PRIMARY KEY NOT NULL,
-            title TEXT NOT NULL,
-            author TEXT NOT NULL,
-            body TEXT NOT NULL
-        )",
-                rusqlite::NO_PARAMS,
-            ),
-            "Could not initialize DB",
-        );
+        Self {
+            conn: FdLock::new(file),
+        }
+    }
+}
 
+impl Posts {
+    pub fn get_all(path: &str) -> Self {
         if *conf::DEBUG {
-            log::info!(
-                "Database connection established in {}ms",
-                start.elapsed().as_millis()
-            );
+            log::info!("Retrieving posts...");
         }
 
-        conn
+        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
     }
 
-    pub fn new() -> Self {
-        Conn {
-            conn: Conn::init(DB_PATH),
-        }
+    pub fn replace(&mut self, n: usize, post: Post) {
+        self.posts[n] = post;
+    }
+
+    pub fn get(&self, n: usize) -> &Post {
+        &self.posts[n]
+    }
+
+    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",
+        );
     }
 }
 
@@ -71,10 +97,19 @@ mod tests {
     use super::*;
 
     #[test]
-    fn test_new() {
-        let conn = Conn::init(":memory:");
-        let mut stmt = conn.prepare("SELECT * FROM POSTS").unwrap();
+    fn test_init() {
+        let mut conn = Conn::init(PATH);
+        conn.conn.try_lock().unwrap();
+    }
+
+    #[test]
+    fn retrieve_posts_and_examine() {
+        let all = Posts::get_all(PATH);
+        assert_eq!(all.posts.len(), 1);
 
-        stmt.query_map(rusqlite::NO_PARAMS, |_| Ok(())).unwrap();
+        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'");
     }
 }
diff --git a/src/main.rs b/src/main.rs
index f6d136d..6c38431 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -21,17 +21,15 @@ fn main() {
     println!("a community notices system");
     println!();
 
-    let db = db::Conn::new();
-
     if *conf::DEBUG {
         log::info!("Startup completed in {:?}ms", start.elapsed().as_millis());
     }
 
     if arg_matches.subcommand_matches("post").is_some() {
         log::info!("New post...");
-        error::helper(posts::create(&db), "Error creating new post");
+        error::helper(posts::create(), "Error creating new post");
     } else if let Some(updmatch) = arg_matches.subcommand_matches("update") {
-        let id: u32 = if let Some(val) = updmatch.value_of("id") {
+        let id: usize = if let Some(val) = updmatch.value_of("id") {
             error::helper(val.parse(), "Couldn't parse ID")
         } else {
             0
@@ -40,11 +38,11 @@ fn main() {
         log::info!("Updating post ...");
 
         error::helper(
-            posts::update_handler(&db, id),
+            posts::update_handler(id),
             format!("Error updating post {}", id).as_ref(),
         );
     } else if let Some(delmatch) = arg_matches.subcommand_matches("delete") {
-        let id: u32 = if let Some(val) = delmatch.value_of("id") {
+        let id: usize = if let Some(val) = delmatch.value_of("id") {
             error::helper(val.parse(), "Couldn't parse ID")
         } else {
             0
@@ -53,10 +51,10 @@ fn main() {
         log::info!("Deleting post");
 
         error::helper(
-            posts::delete_handler(&db, id),
+            posts::delete_handler(id),
             format!("Error deleting post {}", id).as_ref(),
         );
     }
 
-    error::helper(posts::display(&db), "Error displaying posts");
+    error::helper(posts::display(), "Error displaying posts");
 }
diff --git a/src/posts.rs b/src/posts.rs
index c9f9307..b286469 100644
--- a/src/posts.rs
+++ b/src/posts.rs
@@ -5,17 +5,6 @@ use crate::ed;
 use crate::error;
 use crate::user;
 
-// Executes the sql statement that inserts a new post
-// Broken off for unit testing.
-pub fn exec_new(stmt: &mut rusqlite::Statement, title: &str, body: &str) -> error::Result<()> {
-    stmt.execute_named(&[
-        (":title", &title),
-        (":author", &*user::NAME),
-        (":body", &body),
-    ])?;
-    Ok(())
-}
-
 // Make sure nobody encodes narsty characters
 // into a message to negatively affect other
 // users
@@ -29,11 +18,7 @@ fn str_to_utf8(str: &str) -> String {
 }
 
 // First handler for creating a new post.
-pub fn create(db: &db::Conn) -> error::Result<()> {
-    let mut stmt = db
-        .conn
-        .prepare("INSERT INTO posts (title, author, body) VALUES (:title, :author, :body)")?;
-
+pub fn create() -> error::Result<()> {
     println!();
     println!("Title of the new post: ");
 
@@ -55,43 +40,38 @@ pub fn create(db: &db::Conn) -> error::Result<()> {
     } else {
         &body_raw
     };
-
     let trimmed_body = body.trim();
 
-    exec_new(&mut stmt, title, trimmed_body)?;
+    let user = &*user::NAME;
+
+    let mut all = db::Posts::get_all(db::PATH);
+    let new = db::Post {
+        author: user.into(),
+        title: title.to_string(),
+        body: trimmed_body.to_string(),
+    };
+
+    all.append(new);
+    all.write();
 
     println!();
     Ok(())
 }
 
 // Shows the most recent posts.
-pub fn display(db: &db::Conn) -> error::Result<()> {
-    let mut stmt = db.conn.prepare("SELECT * FROM posts")?;
-    let out = stmt.query_map(rusqlite::NO_PARAMS, |row| {
-        let id: u32 = row.get(0)?;
-        let title: String = row.get(1)?;
-        let author: String = row.get(2)?;
-        let body: String = row.get(3)?;
-        Ok(db::Post {
-            id,
-            title,
-            author,
-            body,
-        })
-    })?;
+pub fn display() -> error::Result<()> {
+    let all = db::Posts::get_all(db::PATH);
 
     let mut postvec = Vec::new();
-    out.for_each(|row| {
-        if let Ok(post) = row {
-            let newpost = format!(
-                "{}. {} -> by {}\n{}\n\n",
-                post.id,
-                post.title.trim(),
-                post.author,
-                post.body.trim()
-            );
-            postvec.push(newpost);
-        }
+    all.posts.iter().enumerate().for_each(|(id, post)| {
+        let newpost = format!(
+            "{}. {} -> by {}\n{}\n\n",
+            id + 1,
+            post.title.trim(),
+            post.author,
+            post.body.trim()
+        );
+        postvec.push(newpost);
     });
 
     for (i, e) in postvec.iter().enumerate() {
@@ -104,8 +84,8 @@ pub fn display(db: &db::Conn) -> error::Result<()> {
 }
 
 // First handler to update posts.
-pub fn update_handler(db: &db::Conn, id: u32) -> error::Result<()> {
-    let id_num_in = if id == 0 {
+pub fn update_handler(id: usize) -> error::Result<()> {
+    let mut id_num_in = if id == 0 {
         println!();
         println!("ID number of your post to edit?");
         let mut id_num_in = String::new();
@@ -115,31 +95,29 @@ pub fn update_handler(db: &db::Conn, id: u32) -> error::Result<()> {
         id
     };
 
-    let mut get_stmt = db.conn.prepare("SELECT * FROM posts WHERE id = :id")?;
+    id_num_in -= 1;
 
-    let row = get_stmt.query_row_named(&[(":id", &id_num_in)], |row| {
-        let title: String = row.get(1)?;
-        let author = row.get(2)?;
-        let body = row.get(3)?;
-        Ok(vec![title, author, body])
-    })?;
+    let user = &*user::NAME;
+    let mut all = db::Posts::get_all(db::PATH);
+    let post = all.get(id_num_in);
 
-    if *user::NAME != row[1] {
+    if *user != post.author {
+        println!();
+        println!("Users don't match. Can't update post!");
         println!();
-        println!("Username mismatch - can't update_handler post!");
-        return Ok(());
+        std::process::exit(1);
     }
 
     let mut new_title = String::new();
 
     println!("Updating post {}", id_num_in);
     println!();
-    println!("Current Title: {}", &row[0]);
+    println!("Current Title: {}", post.title);
     println!();
     println!("Enter new title:");
     io::stdin().read_line(&mut new_title)?;
 
-    let body_raw = str_to_utf8(&ed::call(&row[2]));
+    let body_raw = str_to_utf8(&ed::call(&post.body));
     let body = if body_raw.len() > 500 {
         &body_raw[..500]
     } else {
@@ -148,38 +126,24 @@ pub fn update_handler(db: &db::Conn, id: u32) -> error::Result<()> {
 
     let trimmed_body = body.trim();
 
-    update(&new_title, &trimmed_body, id_num_in, &db)?;
-
-    println!();
-    Ok(())
-}
-
-// Allows editing of posts - called by main::update
-pub fn update(new_title: &str, new_body: &str, id_num_in: u32, db: &db::Conn) -> error::Result<()> {
-    let new_title = new_title.trim();
-    let new_body = new_body.trim();
-
-    let title_stmt = format!("UPDATE posts SET title = :title WHERE id = {}", id_num_in);
-    let mut stmt = db.conn.prepare(&title_stmt)?;
-    stmt.execute_named(&[(":title", &new_title)])?;
-    let body_stmt = format!("UPDATE posts SET body = :body WHERE id = {}", id_num_in);
-    let mut stmt = db.conn.prepare(&body_stmt)?;
+    all.replace(
+        id_num_in,
+        db::Post {
+            author: user.into(),
+            title: new_title,
+            body: trimmed_body.to_string(),
+        },
+    );
 
-    stmt.execute_named(&[(":body", &new_body)])?;
-
-    Ok(())
-}
-
-// Helper to just run a sql statement.
-pub fn exec_stmt_no_params(stmt: &mut rusqlite::Statement) -> error::Result<()> {
-    stmt.execute(rusqlite::NO_PARAMS)?;
+    all.write();
 
+    println!();
     Ok(())
 }
 
 // First handler to remove a post
-pub fn delete_handler(db: &db::Conn, id: u32) -> error::Result<()> {
-    let id_num_in: u32 = if id == 0 {
+pub fn delete_handler(id: usize) -> error::Result<()> {
+    let mut id_num_in = if id == 0 {
         println!();
         println!("ID of the post to delete?");
         let mut id_num_in = String::new();
@@ -190,53 +154,20 @@ pub fn delete_handler(db: &db::Conn, id: u32) -> error::Result<()> {
         id
     };
 
-    let del_stmt = format!("DELETE FROM posts WHERE id = {}", id_num_in);
-    let get_stmt = format!("SELECT * FROM posts WHERE id = {}", id_num_in);
-
-    let mut get_stmt = db.conn.prepare(&get_stmt)?;
-    let mut del_stmt = db.conn.prepare(&del_stmt)?;
+    id_num_in -= 1;
 
-    let user_in_post: String = get_stmt.query_row(rusqlite::NO_PARAMS, |row| row.get(2))?;
+    let mut all = db::Posts::get_all(db::PATH);
+    let post = all.get(id_num_in);
 
-    if *user::NAME != user_in_post {
+    if *user::NAME != post.author {
         println!();
-        println!("Users don't match. Can't delete!");
+        println!("Users don't match. Can't delete post!");
         println!();
-        return Ok(());
+        std::process::exit(1);
     }
 
-    exec_stmt_no_params(&mut del_stmt)?;
-    Ok(())
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn post_new() {
-        let db = db::Conn::init(":memory:");
-        let db = db::Conn { conn: db };
-        let mut stmt = db
-            .conn
-            .prepare("INSERT INTO posts (title, author, body) VALUES (:title, :author, :body)")
-            .unwrap();
-
-        let title = String::from("TEST TITLE");
-
-        exec_new(&mut stmt, &title, "TEST BODY").unwrap();
-        update("NEW TITLE", "TEST BODY", 1, &db).unwrap();
+    all.delete(id_num_in);
+    all.write();
 
-        let mut stmt = db
-            .conn
-            .prepare("SELECT * FROM posts WHERE title = :title")
-            .unwrap();
-
-        let title = String::from("NEW TITLE");
-        let out: String = stmt
-            .query_row_named(&[(":title", &title)], |row| row.get::<usize, String>(1))
-            .unwrap();
-
-        assert_eq!("NEW TITLE", &out);
-    }
+    Ok(())
 }