~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)
}
}