use std::ffi::{OsStr, OsString}; #[cfg(target_family="unix")] use std::os::unix::ffi::OsStrExt; #[cfg(target_family="windows")] use std::os::windows::ffi::OsStrExt; use std::sync::Mutex; use std::path::Path; use rusqlite::{Connection, params}; // in some configurations (`main.rs`), nothing actually uses id. #[allow(dead_code)] pub struct Project { pub id: u32, pub name: String, pub dir: OsString, pub host: Option, pub info: Option, pub upstream: Option, } impl Project { pub fn is_here(&self) -> bool { let uname = rustix::system::uname(); let me = uname.nodename(); if let Some(host) = self.host.as_ref() { host.as_bytes() == me.to_bytes() } else { std::path::Path::new(&self.dir).exists() } } } pub struct DbCtx { pub conn: Mutex, } impl DbCtx { pub fn new>(db_path: P) -> Result { let conn = Connection::open(db_path) .map_err(|e| format!("failed to open db: {e}"))?; Ok(Self { conn: Mutex::new(conn) }) } pub fn init>(db_path: P) -> Result<(), String> { let conn = Connection::open(db_path) .map_err(|e| format!("failed to open db: {e}"))?; conn.execute( "CREATE TABLE projects (\ id INTEGER PRIMARY KEY AUTOINCREMENT, \ name TEXT UNIQUE NOT NULL, \ dir BLOB NOT NULL, \ host TEXT, \ info TEXT, \ upstream TEXT\ );", params![]) .map_err(|e| format!("failed to create projects table: {e}"))?; conn.execute( "CREATE INDEX projname on projects(name);", params![]) .map_err(|e| format!("failed to create index?!: {e}"))?; Ok(()) } pub fn project_add(&self, p: Project) -> Result<(), String> { let conn = self.conn.lock().unwrap(); conn .execute( "insert into projects \ (name, dir, host, info, upstream) \ values (?1, ?2, ?3, ?4, ?5);",/* \ on conflict (name) do update \ set dir=excluded.dir \ info=excluded.info \ upstream=excluded.upstream;",*/ params![p.name, p.dir.as_os_str().as_bytes(), p.host, p.info, p.upstream] ) .map_err(|e| format!("failed to insert project: {e}"))?; Ok(()) } pub fn project_update(&self, p: &Project) -> Result<(), String> { let conn = self.conn.lock().unwrap(); // TODO: don't panic on name conflict.. conn .execute( "update projects \ set name=?2, \ dir=?3, \ host=?4, \ info=?5, \ upstream=?6 \ where id=?1", params![p.id.to_string(), p.name, p.dir.as_os_str().as_bytes(), p.host, p.info, p.upstream] ) .map_err(|e| format!("failed to update project: {e}"))?; Ok(()) } pub fn project_forget(&self, name: &str) -> Result<(), String> { let conn = self.conn.lock().unwrap(); conn .execute( "delete from projects where name=?1;", params![name] ) .map_err(|e| format!("failed to delete project: {e}"))?; Ok(()) } pub fn for_each_project(&self, op: impl Fn(Project)) -> Result<(), String> { let conn = self.conn.lock().unwrap(); let mut stmt = conn.prepare("select * from projects").expect("can prepare"); let mut rows = stmt.query([]) .map_err(|e| format!("failed to select projects: {e}"))?; while let Some(row) = rows.next().expect("can read row") { let id: u32 = row.get(0).expect("typecheck"); let dir: Box<[u8]> = row.get(2).expect("typecheck"); let osstr: &OsStr = OsStrExt::from_bytes(&dir); let project = Project { id, name: row.get(1).expect("typecheck"), dir: osstr.to_os_string(), host: row.get(3).expect("typecheck"), info: row.get(4).expect("typecheck"), upstream: row.get(5).expect("typecheck"), }; op(project); } Ok(()) } pub fn project_by_name(&self, name: &str) -> Result, String> { let conn = self.conn.lock().unwrap(); let mut stmt = conn.prepare("select id, name, dir, host, info, upstream from projects where name=?1") .expect("can prepare"); let mut rows = stmt.query([name]) .map_err(|e| format!("failed to find projects: {e}"))?; let project = if let Some(row) = rows.next().expect("can read row") { let id: u32 = row.get(0).expect("typecheck"); let dir: Box<[u8]> = row.get(2).expect("typecheck"); let osstr: &OsStr = OsStrExt::from_bytes(&dir); Some(Project { id, name: row.get(1).expect("typecheck"), dir: osstr.to_os_string(), host: row.get(3).expect("typecheck"), info: row.get(4).expect("typecheck"), upstream: row.get(5).expect("typecheck"), }) } else { None }; assert!(rows.next().expect("can try getting next row").is_none()); Ok(project) } }