/* * 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(u: X, r: Y) -> String where String: From, String: From, { 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 { 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 { 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::(0) { Ok(x) => x, Err(e) => return query_failed!("error getting id: {e}"), }; let name = match s.read::(1) { Ok(x) => x, Err(e) => return query_failed!("error getting name: {e}"), }; let hash = match s.read::(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, 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::(0) { Ok(x) => x, Err(e) => return query_failed!("error getting id: {e}"), }; let name = match s.read::(1) { Ok(x) => x, Err(e) => return query_failed!("error getting name: {e}"), }; let hash = match s.read::(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 { 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::(0) { Ok(x) => x, Err(e) => return query_failed!("error getting id: {e}"), }; let name = match s.read::(1) { Ok(x) => x, Err(e) => return query_failed!("error getting name: {e}"), }; let hash = match s.read::(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 { 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 { 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::(0) { Ok(x) => x, Err(e) => return err_fmt!("failed to get id: {e}"), }; let uid = match s.read::(1) { Ok(x) => x, Err(e) => return err_fmt!("failed to get uid: {e}"), }; let hash = match s.read::(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, 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 = Vec::new(); while let Ok(State::Row) = s.next() { let _i = match s.read::(0) { Ok(x) => x, Err(e) => return query_failed!("failed to get id: {e}"), }; let _u = match s.read::(1) { Ok(x) => x, Err(e) => return query_failed!("failed to get name: {e}"), }; let r = match s.read::(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, 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 = Vec::new(); while let Ok(State::Row) = s.next() { let i = match s.read::(0) { Ok(x) => x, Err(e) => return query_failed!("failed to get id: {e}"), }; let n = match s.read::(1) { Ok(x) => x, Err(e) => return query_failed!("failed to get name: {e}"), }; let d = match s.read::(2) { Ok(x) => x, Err(e) => { return query_failed!("failed to get description: {e}") } }; let oid = match s.read::(3) { Ok(x) => x, Err(e) => return query_failed!("failed to get owner id: {e}"), }; let settings = match s.read::(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 { 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 { 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::(0) { Ok(x) => x, Err(e) => return err_fmt!("failed to get id: {e}"), }; let n = match s.read::(1) { Ok(x) => x, Err(e) => return err_fmt!("failed to get name: {e}"), }; let d = match s.read::(2) { Ok(x) => x, Err(e) => return err_fmt!("failed to get description: {e}"), }; let oid = match s.read::(3) { Ok(x) => x, Err(e) => return err_fmt!("failed to get oid: {e}"), }; let s = match s.read::(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 { 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::(0) { Ok(x) => x, Err(e) => return err_fmt!("failed to get id: {e}"), }; let n = match s.read::(1) { Ok(x) => x, Err(e) => return err_fmt!("failed to get name: {e}"), }; let d = match s.read::(2) { Ok(x) => x, Err(e) => return err_fmt!("failed to get description: {e}"), }; let oid = match s.read::(3) { Ok(x) => x, Err(e) => return err_fmt!("failed to get oid: {e}"), }; let settings = match s.read::(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 { /* 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::(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 { /* 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 { 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::(0) { Ok(x) => x, Err(e) => return err_fmt!("failed to get id: {e}"), }; let p = match s.read::(1) { Ok(x) => x, Err(e) => return err_fmt!("failed to get private: {e}"), }; let o = match s.read::(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) } }