use anyhow::{Context, Result};
use rusqlite::{Connection, params};
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use crate::core::paths::{canonical, kaizen_dir};
mod connection;
mod legacy;
mod metadata;
mod sql;
const MACHINE_DB: &str = "machine.db";
pub fn db_path() -> Option<PathBuf> {
kaizen_dir().map(|d| d.join(MACHINE_DB))
}
fn db_path_for_write(workspace: &Path) -> Result<Option<PathBuf>> {
if kaizen_dir().is_none() {
return Ok(None);
}
crate::core::home_paths::sqlite_file_for_write(workspace, MACHINE_DB).map(Some)
}
fn now_ms() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64
}
fn name_for_path(path: &Path) -> String {
path.file_name()
.and_then(|n| n.to_str())
.map(str::to_string)
.unwrap_or_default()
}
fn open_conn_write(workspace: &Path) -> Result<Option<Connection>> {
connection::open_write(workspace)
}
fn open_conn_read() -> Result<Option<Connection>> {
connection::open_read()
}
fn with_write<F>(workspace: &Path, f: F) -> Result<()>
where
F: FnOnce(&Connection) -> Result<()>,
{
let Some(conn) = open_conn_write(workspace)? else {
return Ok(());
};
legacy::migrate(&conn)?;
f(&conn)
}
pub fn upsert_from_resolve(path: &Path) -> Result<()> {
with_write(path, |conn| upsert_seen(conn, path))
}
fn upsert_seen(conn: &Connection, path: &Path) -> Result<()> {
let c = canonical(path);
let t = now_ms();
let name = name_for_path(&c);
let p = c.to_string_lossy();
conn.execute(sql::UPSERT_SEEN, params![p.as_ref(), &name, t, t])
.context("machine registry upsert from resolve")?;
Ok(())
}
pub fn record_init(path: &Path) -> Result<()> {
with_write(path, |conn| insert_init(conn, path))
}
fn insert_init(conn: &Connection, path: &Path) -> Result<()> {
let c = canonical(path);
let t = now_ms();
let name = name_for_path(&c);
let p = c.to_string_lossy();
let origin = metadata::git_remote_origin(&c);
let values = params![
p.as_ref(),
&name,
t,
t,
t,
origin.as_deref(),
env!("CARGO_PKG_VERSION")
];
conn.execute(sql::RECORD_INIT, values)
.context("machine registry record init")?;
Ok(())
}
pub fn list_paths() -> Result<Vec<PathBuf>> {
Ok(list_paths_including_missing()?
.into_iter()
.filter(|path| path.is_dir())
.collect())
}
pub fn list_paths_including_missing() -> Result<Vec<PathBuf>> {
let mut paths = legacy::read_paths();
if let Some(conn) = open_conn_read()? {
extend_unique(&mut paths, query_paths(&conn)?);
}
Ok(paths)
}
fn extend_unique(paths: &mut Vec<PathBuf>, additions: impl IntoIterator<Item = PathBuf>) {
additions.into_iter().for_each(|path| {
if !paths.contains(&path) {
paths.push(path);
}
});
}
fn query_paths(conn: &Connection) -> Result<Vec<PathBuf>> {
let mut stmt = conn
.prepare(sql::LIST_PATHS)
.context("machine registry list paths")?;
let rows = stmt
.query_map([], |row| row.get::<_, String>(0).map(PathBuf::from))
.context("query machine registry")?;
Ok(rows.collect::<rusqlite::Result<_>>()?)
}
pub fn is_registered(path: &Path) -> bool {
let canonical = canonical(path);
if legacy::read_paths().contains(&canonical) {
return true;
}
let Some(conn) = open_conn_read().ok().flatten() else {
return false;
};
registered(&conn, &canonical)
}
fn registered(conn: &Connection, path: &Path) -> bool {
let c = canonical(path);
let p = c.to_string_lossy();
conn.query_row(sql::IS_REGISTERED, [p.as_ref()], |_| Ok(()))
.is_ok()
}
pub fn status() -> Result<Option<(PathBuf, usize)>> {
let Some(path) = db_path() else {
return Ok(None);
};
let legacy = legacy::read_paths();
let conn = open_conn_read()?;
Ok(Some((path, total_project_count(conn.as_ref(), &legacy))))
}
fn total_project_count(conn: Option<&Connection>, legacy: &[PathBuf]) -> usize {
match conn {
Some(conn) => project_count(conn) + legacy.iter().filter(|p| !registered(conn, p)).count(),
None => legacy.len(),
}
}
fn project_count(conn: &Connection) -> usize {
conn.query_row(sql::PROJECT_COUNT, [], |row| row.get::<_, i64>(0))
.unwrap_or(0) as usize
}