~skye/git/src/db.rs

view raw


/*
 * database wrapper & entry structures
 */

use colored::Colorize;
use pwhash::bcrypt;
use sqlite3::{Connection, State};
use std::process::Command;

use crate::{com, macros::*, now};

#[derive(Debug, Clone)]
pub struct User {
    pub id: i64,
    pub name: String,
    pub hash: String,
}

/** the kinds of errors get_user can return */
#[derive(Debug, Clone)]
pub enum DbErr {
    UserNotFound(String),
    QueryFailed(String),
}

/** unwrap a db err into a string */
pub fn db_err_fmt(x: &DbErr) -> String {
    match x {
        DbErr::UserNotFound(e) => format!("user not found: {e}"),
        DbErr::QueryFailed(e) => format!("database query failed: {e}"),
    }
}

#[derive(Debug, Clone)]
pub struct Session {
    pub id: i64,
    pub uid: i64,
    pub hash: String,
}

#[derive(Debug, Clone)]
pub struct Repo {
    pub id: i64,
    pub name: String,
    pub desc: String,
    pub oid: i64,
    pub settings: i64,
}

#[derive(Debug, Clone)]
pub struct Settings {
    pub id: i64,
    pub private: i64, /* this i64 is actually a bool */
    pub oid: i64,
}

impl Settings {
    pub fn new(oid: i64) -> Self {
        Self {
            id: -1,
            private: 0,
            oid,
        }
    }
}

/** does this really need to be a macro? hard to say. */
#[macro_export]
macro_rules! mkrepopath {
    ( $u:expr, $n:expr ) => {
        format!("/home/{u}/{n}", u = $u, n = $n)
    };
}

/** fmt for db log.
 * $p: prefix */
macro_rules! db_log_fmt {
    ($p:expr, $($t:tt)*) => {{
        format!(
            "{}[{}]: {}",
            log_fmt!("{}", $p),
            "db".green(),
            format!($($t)*)
        )
    }};
}

/** log wrapper for the db specifically */
macro_rules! db_log {
    ($($t:tt)*) => {{
        use colored::Colorize;
        println!("{}", db_log_fmt!("log".blue(), $($t)*));
    }};
}

/** db log but stderr */
macro_rules! db_elog {
    ($($t:tt)*) => {{
        eprintln!("{}", db_log_fmt!("err".red(), $($t)*));
    }};
}

/** db log but for dbg */
macro_rules! db_dbg_log {
    ($($t:tt)*) => {{
        println!("{}", db_log_fmt!("dbg".purple(), $($t)*));
    }};
}

/** shortcut to format, log, and prepare */
macro_rules! prepare {
    ($x:expr, $($t:tt)*) => {{
        let q = format!($($t)*);
        db_log!("prepare!: running query:\n\t{}", q.trim());
        $x.con.prepare(q)
    }};
}

/** shortcut to format, log, and execute */
macro_rules! execute {
    ($x:expr, $($t:tt)*) => {{
        let q = format!($($t)*);
        db_log!("execute!: running query:\n\t{}", q.trim());
        $x.con.execute(q)
    }};
}

/** construct and format a query err */
macro_rules! query_failed {
    ($($t:tt)*) => {{
        Err(DbErr::QueryFailed(format!($($t)*)))
    }};
}

/** construct and format a user not found err */
macro_rules! user_not_found {
    ($($t:tt)*) => {{
        Err(DbErr::QueryFailed(format!($($t)*)))
    }};
}

/** format into a repo name */
fn repo_name<X, Y>(u: X, r: Y) -> String
where
    String: From<X>,
    String: From<Y>,
{
    let (u, r) = (String::from(u), String::from(r));
    format!("~{u}/{r}")
}


pub struct Database {
    file: String,
    con: Connection,
}

impl Clone for Database {
    fn clone(&self) -> Self {
        Database::new(&self.file).expect("new database")
    }
}

/* TODO: remove ALL unwraps in non-init functions
 * if the db crashes we're fucked */
impl Database {
    pub fn new(f: &str) -> sqlite3::Result<Self> {
        let c = sqlite3::open(f)?;

        c.execute(
            r#"
            create table if not exists users (
                id integer primary key autoincrement,
                name text not null,
                hash text not null
            );
            create table if not exists sessions (
                id integer primary key autoincrement,
                uid integer not null,
                hash text not null
            );
            create table if not exists repos (
                id integer primary key autoincrement,
                name text not null,
                desc text not null,
                -- owner id
                oid integer not null,
                settings integer
            );
            create table if not exists settings (
                id integer primary key autoincrement,
                private integer not null,
                oid integer not null
            );
            create table if not exists pinned (
                id integer primary key autoincrement,
                uid integer not null,
                rid integer not null
            );
            "#,
        )?;

        Ok(Self {
            file: String::from(f),
            con: c,
        })
    }

    pub fn user_exists(&self, n: &str) -> bool {
        let mut s = {
            match prepare!(self, "select id from users where name = '{n}'") {
                Ok(x) => x,
                Err(e) => {
                    /* we can't return err_fmt bc the
                     * function just returns a bool
                     */
                    db_elog!("error running query: {e}");
                    return false;
                }
            }
        };

        while let Ok(State::Row) = s.next() {
            return true;
        }
        return false;
    }

    pub fn add_user(&self, n: &str, p: &str) -> Result<(), String> {
        let h = match bcrypt::hash(p) {
            Ok(x) => x,
            Err(e) => return err_fmt!("failed to encrypt password {p}: {e}"),
        };

        match self.con.execute(format!(
            "insert into users (name, hash) values ('{n}', '{h}');"
        )) {
            Ok(x) => x,
            Err(e) => {
                return err_fmt!("failed to add user {n} to database: {e}");
            }
        };
        match Command::new("etc/new-user.sh").args([n, p]).output() {
            Ok(x) => db_log!(
                "new-user.sh output:\n{:?}",
                std::str::from_utf8(&x.stdout)
            ),
            Err(e) => return err_fmt!("failed to change user's password: {e}"),
        };
        Ok(())
    }

    pub fn get_user(&self, n: &str) -> Result<User, DbErr> {
        db_log!("getting user info for {n}");

        let mut s = {
            match prepare!(self, "select * from users where name = '{n}'") {
                Ok(x) => x,
                Err(e) => return query_failed!("failed to prepare(): {e}"),
            }
        };

        while let Ok(State::Row) = s.next() {
            /* TODO: should probably make sure there's only
             * one row returned */
            let id = match s.read::<i64>(0) {
                Ok(x) => x,
                Err(e) => return query_failed!("error getting id: {e}"),
            };

            let name = match s.read::<String>(1) {
                Ok(x) => x,
                Err(e) => return query_failed!("error getting name: {e}"),
            };

            let hash = match s.read::<String>(2) {
                Ok(x) => x,
                Err(e) => return query_failed!("error getting hash: {e}"),
            };

            return Ok(User { id, name, hash });
        }

        user_not_found!("user {n} not found")
    }

    pub fn get_users(&self) -> Result<Vec<User>, DbErr> {
        let mut s = match prepare!(self, "select * from users") {
            Ok(x) => x,
            Err(e) => return query_failed!("failed to prepare(): {e}"),
        };

        let mut r = Vec::new();

        while let Ok(State::Row) = s.next() {
            let id = match s.read::<i64>(0) {
                Ok(x) => x,
                Err(e) => return query_failed!("error getting id: {e}"),
            };

            let name = match s.read::<String>(1) {
                Ok(x) => x,
                Err(e) => return query_failed!("error getting name: {e}"),
            };

            let hash = match s.read::<String>(2) {
                Ok(x) => x,
                Err(e) => return query_failed!("error getting hash: {e}"),
            };

            r.push(User { id, name, hash });
        }

        Ok(r)
    }

    pub fn get_user_by_id(&self, i: i64) -> Result<User, DbErr> {
        let mut s = {
            match prepare!(self, "select * from users where id = {i}") {
                Ok(x) => x,
                Err(e) => return query_failed!("error querying database: {e}"),
            }
        };

        while let Ok(State::Row) = s.next() {
            let id = match s.read::<i64>(0) {
                Ok(x) => x,
                Err(e) => return query_failed!("error getting id: {e}"),
            };

            let name = match s.read::<String>(1) {
                Ok(x) => x,
                Err(e) => return query_failed!("error getting name: {e}"),
            };

            let hash = match s.read::<String>(2) {
                Ok(x) => x,
                Err(e) => return query_failed!("error getting hash: {e}"),
            };

            return Ok(User { id, name, hash });
        }

        user_not_found!("user with id {i} not found")
    }

    pub fn add_session(&self, u: i64) -> Result<String, String> {
        let mut h: String = com::mkhash();
        let mut s = {
            match {
                prepare!(self, "select id from sessions where hash = '{h}'")
            } {
                Ok(x) => x,
                Err(e) => return err_fmt!("error querying database: {e}"),
            }
        };

        /* TODO: this should check the db a second time
         * although this is exceptionally unlikely to
         * be an issue regardless */
        while let Ok(State::Row) = s.next() {
            h = com::mkhash();
        }

        match self.con.execute(format!(
            "insert into sessions (uid, hash) values ({u}, '{h}');"
        )) {
            Ok(x) => x,
            Err(e) => {
                let s = format!("error adding session: {e}");
                elog!("{s}");
                return Err(s);
            }
        };

        Ok(h)
    }

    pub fn get_session(&self, sid: &str) -> Result<Session, String> {
        let mut s = {
            match prepare!(
                self,
                r#"
                select * from sessions where hash = '{sid}'
            "#
            ) {
                Ok(x) => x,
                Err(e) => return err_fmt!("failed to query db: {e}"),
            }
        };

        while let Ok(State::Row) = s.next() {
            let id = match s.read::<i64>(0) {
                Ok(x) => x,
                Err(e) => return err_fmt!("failed to get id: {e}"),
            };

            let uid = match s.read::<i64>(1) {
                Ok(x) => x,
                Err(e) => return err_fmt!("failed to get uid: {e}"),
            };

            let hash = match s.read::<String>(2) {
                Ok(x) => x,
                Err(e) => return err_fmt!("failed to get hash: {e}"),
            };

            return Ok(Session { id, uid, hash });
        }

        err_fmt!("session {sid} not found")
    }

    pub fn add_pin(&self, u: i64, r: i64) -> Result<(), String> {
        match execute!(self, "insert into pinned (uid,rid) values ({u},{r})") {
            Ok(_) => Ok(()),
            Err(e) => {
                err_fmt!(
                    "failed to insert pinned with oid {u} and rid {r}: {e}"
                )
            }
        }
    }

    pub fn add_repo(&self, n: &str, d: &str, o: i64) -> Result<(), String> {
        match self.con.execute(format!(
            "insert into repos (name,desc,oid) values ('{n}','{d}',{o});"
        )) {
            Ok(_) => Ok(()),
            Err(e) => {
                let m = format!("failed to add repo {n} to database: {e}");
                elog!("{m}");
                Err(m)
            }
        }
    }

    pub fn get_pinned_repos(&self, o: i64) -> Result<Vec<Repo>, DbErr> {
        let mut s = {
            match prepare!(self, "select * from pinned where uid = {o}") {
                Ok(x) => x,
                Err(e) => {
                    return query_failed!("failed to query database: {e}");
                }
            }
        };

        let mut v: Vec<Repo> = Vec::new();

        while let Ok(State::Row) = s.next() {
            let _i = match s.read::<i64>(0) {
                Ok(x) => x,
                Err(e) => return query_failed!("failed to get id: {e}"),
            };

            let _u = match s.read::<i64>(1) {
                Ok(x) => x,
                Err(e) => return query_failed!("failed to get name: {e}"),
            };

            let r = match s.read::<i64>(2) {
                Ok(x) => x,
                Err(e) => return query_failed!("failed to get desc: {e}"),
            };

            v.push(match self.get_repo_by_id(r) {
                Ok(x) => x,
                Err(e) => return query_failed!("failed to get repo: {e}"),
            });
        }

        Ok(v)
    }

    pub fn get_user_repos(&self, o: i64) -> Result<Vec<Repo>, DbErr> {
        let mut s = {
            match prepare!(self, "select * from repos where oid = {o}") {
                Ok(x) => x,
                Err(e) => {
                    return query_failed!("failed to query database: {e}")
                }
            }
        };

        let mut v: Vec<Repo> = Vec::new();

        while let Ok(State::Row) = s.next() {
            let i = match s.read::<i64>(0) {
                Ok(x) => x,
                Err(e) => return query_failed!("failed to get id: {e}"),
            };

            let n = match s.read::<String>(1) {
                Ok(x) => x,
                Err(e) => return query_failed!("failed to get name: {e}"),
            };

            let d = match s.read::<String>(2) {
                Ok(x) => x,
                Err(e) => {
                    return query_failed!("failed to get description: {e}")
                }
            };

            let oid = match s.read::<i64>(3) {
                Ok(x) => x,
                Err(e) => return query_failed!("failed to get owner id: {e}"),
            };

            let settings = match s.read::<i64>(4) {
                Ok(x) => x,
                Err(e) => return query_failed!("failed to get settings: {e}"),
            };

            v.push(Repo {
                id: i,
                name: n,
                desc: d,
                oid,
                settings,
            });
        }

        Ok(v)
    }

    pub fn repo_exists(&self, o: i64, n: &str) -> bool {
        let mut s = {
            match prepare!(
                self,
                r#"
                select id from repos
                where name = '{}' and oid = {}
                "#,
                n,
                o
            ) {
                Ok(x) => x,
                Err(e) => {
                    elog!("error running query: {e}");
                    return false;
                }
            }
        };

        while let Ok(State::Row) = s.next() {
            return true;
        }
        return false;
    }

    pub fn repo_is_private(&mut self, i: i64) -> Result<bool, String> {
        match self.get_settings(i) {
            Ok(r) => Ok(com::i64_to_bool(r.private)),
            Err(e) => err_fmt!("failed to get repo: {e}"),
        }
    }

    pub fn get_repo_by_id(&self, i: i64) -> Result<Repo, String> {
        let q = format!("select * from repos where id = {i}");
        db_log!("query: {q}");
        let mut s = match self.con.prepare(q) {
            Ok(x) => x,
            Err(e) => return err_fmt!("failed to query database: {e}"),
        };

        while let Ok(State::Row) = s.next() {
            let i = match s.read::<i64>(0) {
                Ok(x) => x,
                Err(e) => return err_fmt!("failed to get id: {e}"),
            };

            let n = match s.read::<String>(1) {
                Ok(x) => x,
                Err(e) => return err_fmt!("failed to get name: {e}"),
            };

            let d = match s.read::<String>(2) {
                Ok(x) => x,
                Err(e) => return err_fmt!("failed to get description: {e}"),
            };

            let oid = match s.read::<i64>(3) {
                Ok(x) => x,
                Err(e) => return err_fmt!("failed to get oid: {e}"),
            };

            let s = match s.read::<i64>(4) {
                Ok(x) => x,
                Err(e) => return err_fmt!("failed to get settings: {e}"),
            };

            return Ok(Repo {
                id: i,
                name: n,
                desc: d,
                oid,
                settings: s,
            });
        }

        err_fmt!("no repo found with id {i}")
    }

    pub fn get_repo(&self, o: i64, n: &str) -> Result<Repo, String> {
        let q = format!("select * from repos where name = '{n}' and oid = {o}");
        db_log!("{}: running query: {q}", "get_repo".yellow());
        let mut s = match self.con.prepare(q) {
            Ok(x) => x,
            Err(e) => return err_fmt!("error in prepare(): {e}"),
        };

        while let Ok(State::Row) = s.next() {
            let i = match s.read::<i64>(0) {
                Ok(x) => x,
                Err(e) => return err_fmt!("failed to get id: {e}"),
            };

            let n = match s.read::<String>(1) {
                Ok(x) => x,
                Err(e) => return err_fmt!("failed to get name: {e}"),
            };

            let d = match s.read::<String>(2) {
                Ok(x) => x,
                Err(e) => return err_fmt!("failed to get description: {e}"),
            };

            let oid = match s.read::<i64>(3) {
                Ok(x) => x,
                Err(e) => return err_fmt!("failed to get oid: {e}"),
            };

            let settings = match s.read::<i64>(4) {
                Ok(x) => x,
                Err(e) => return err_fmt!("failed to get settings: {e}"),
            };

            return Ok(Repo {
                id: i,
                name: n,
                desc: d,
                oid,
                settings,
            });
        }

        err_fmt!("repo not found: oid: {o}, name: {n}")
    }

    pub fn new_settings(&mut self, i: i64) -> Result<Settings, String> {
        /* get the repo */
        let r = self.get_repo_by_id(i)?;
        let u = match self.get_user_by_id(r.oid) {
            Ok(x) => x,
            Err(e) => return Err(db_err_fmt(&e)),
        };

        /* make new settings if they don't exist */
        match execute!(
            self,
            "insert into settings (private, oid) values ({}, {})",
            0,
            r.id
        ) {
            Ok(_) => {
                db_log!("inserted settings success")
            }
            Err(e) => return err_fmt!("failed to insert settings: {e}"),
        };

        /* update the repo entry to point to the settings */
        let q = "select * from settings where id = last_insert_rowid();";
        match prepare!(self, "{}", q) {
            Ok(mut s) => {
                let s = (|| {
                    while let Ok(State::Row) = s.next() {
                        return match s.read::<i64>(0) {
                            Ok(x) => return Ok(x),
                            Err(e) => err_fmt!("error reading id: {e}"),
                        };
                    }

                    err_fmt!("new settings not found")
                })()?;

                /* update the repo */
                let q = format!(
                    r#"
                    update repos
                    set settings = {s}
                    where
                        id = {}
                    "#,
                    r.id,
                );
                db_dbg_log!("update query: {}", q.blue());

                match self.con.execute(q) {
                    Ok(x) => {
                        db_log!("updated repo settings");
                        x
                    }
                    Err(e) => {
                        return err_fmt!("failed to update repo: {e}");
                    }
                };
            }
            Err(e) => return err_fmt!("last_insert_rowid failed: {e}"),
        };

        /* get the id of the new entry */
        match self.get_settings(r.id) {
            Ok(x) => {
                db_dbg_log!("updated settings: {:?}", x);
                Ok(Settings {
                    id: x.id,
                    oid: u.id,
                    private: x.private,
                })
            }
            Err(e) => err_fmt!("failed to get new settings: {e}"),
        }
    }

    pub fn set_settings(&mut self, s: Settings) -> Result<Settings, String> {
        /* repo struct */
        let r = match self.get_repo_by_id(s.oid) {
            Ok(x) => x,
            Err(e) => return err_fmt!("repo with id {} not found: {e}", s.oid),
        };
        /* user */
        let u = match self.get_user_by_id(r.oid) {
            Ok(x) => x,
            Err(DbErr::UserNotFound(e)) => {
                return err_fmt!("user with id {} not found: {e}", r.oid)
            }
            Err(DbErr::QueryFailed(e)) => {
                return err_fmt!("query failed when getting user: {e}")
            }
        };
        /* repo name */
        let rn = repo_name(&u.name, &r.name);
        db_log!("updating settings for {rn} to");
        println!("settings: {}", format!("{s:#?}").blue());
        db_dbg_log!(
            "\nuser: {}\nrepo: {}",
            format!("{u:#?}").blue(),
            format!("{r:#?}").blue(),
        );

        /* check if a setting entry exists for this repo */
        match self.get_settings(r.id) {
            Ok(_) => {
                /* check if settings already exist for this repo */
                let q = format!(
                    r#"
                    update settings
                    set private = {p}
                    where
                        id = {i}
                    "#,
                    p = s.private,
                    i = s.id
                );

                /* update */
                match self.con.execute(q) {
                    Ok(_) => {}
                    Err(e) => return err_fmt!("error updating settings: {e}"),
                }
            }
            Err(e) => {
                elog!("settings not found: {e}. creating new entry.");
                self.new_settings(r.id)?;
            }
        }

        /* get the id of the new entry */
        match self.get_settings(r.id) {
            Ok(x) => {
                db_dbg_log!("updated settings: {:?}", x);
                Ok(Settings {
                    id: x.id,
                    oid: u.id,
                    private: x.private,
                })
            }
            Err(e) => err_fmt!("failed to get new settings: {e}"),
        }
    }

    /** note: i here is the id of the *repo*, not the settings entry */
    pub fn get_settings(&mut self, i: i64) -> Result<Settings, String> {
        db_log!("getting repo with id {i}");
        let repo = match self.get_repo_by_id(i) {
            Ok(x) => x,
            Err(e) => return err_fmt!("repo not found: {e}"),
        };
        db_dbg_log!("repo: {:?}", repo);

        let slf = self.clone();
        let mut s = {
            let s = repo.settings;
            match prepare!(slf, "select * from settings where id = {s}") {
                Ok(x) => x,
                Err(e) => return err_fmt!("failed to query db: {e}"),
            }
        };

        while let Ok(State::Row) = s.next() {
            let id = match s.read::<i64>(0) {
                Ok(x) => x,
                Err(e) => return err_fmt!("failed to get id: {e}"),
            };

            let p = match s.read::<i64>(1) {
                Ok(x) => x,
                Err(e) => return err_fmt!("failed to get private: {e}"),
            };

            let o = match s.read::<i64>(2) {
                Ok(x) => x,
                Err(e) => return err_fmt!("failed to get owner id: {e}"),
            };

            return Ok(Settings {
                id,
                private: p,
                oid: o,
            });
        }

        self.new_settings(i)
    }
}