use sqlite::{Connection, State}; use crate::BString; use crate::date::DateTime; use std::env::current_dir; use std::i64; use std::path::PathBuf; fn db_file() -> PathBuf { crate::basedir::data_dir().join("history.db") } #[derive(Clone)] pub struct HistoryEntry { /// time of execution pub time: DateTime, /// absolute path where the command was executed pub loc: BString, /// the command pub cmd: BString, } pub fn canonical_path(mut path: BString) -> BString { while let Some(b'/') = path.last() { path.pop(); } if path.is_empty() { path.push(b'/'); } path } impl HistoryEntry { pub fn new(cmd: BString) -> Self { Self { time: DateTime::now(), loc: canonical_path( current_dir() .unwrap() .as_os_str() .as_encoded_bytes() .to_vec(), ), cmd, } } } fn try_db() -> sqlite::Result { sqlite::open(db_file()) } fn db() -> Connection { try_db().unwrap() } pub fn setup() { let db = db(); db.execute( " CREATE TABLE IF NOT EXISTS history ( id INTEGER PRIMARY KEY AUTOINCREMENT, ts INTEGER NOT NULL, loc BLOB NOT NULL, cmd BLOB NOT NULL ) ", ) .unwrap(); db.execute("CREATE INDEX IF NOT EXISTS idx_history_ts ON history(ts)") .unwrap(); } fn try_persist(entry: &HistoryEntry) -> sqlite::Result<()> { let db = try_db()?; let mut s = db.prepare("INSERT INTO history (ts, loc, cmd) VALUES (?, ?, ?)")?; s.bind((1, entry.time.unix() as i64))?; s.bind((2, entry.loc.as_slice()))?; s.bind((3, entry.cmd.as_slice()))?; s.next()?; Ok(()) } pub fn persist(entry: &HistoryEntry) { // keep quiet in case db fails // TODO maybe better behavior? let _ = try_persist(entry); } pub struct HistoryQueryer { db: Connection, } impl HistoryQueryer { pub fn new() -> sqlite::Result { Ok(Self { db: try_db()? }) } pub fn query( &self, min_time: Option, max_time: Option, path_prefix: Option<&[u8]>, path_strict: bool, ) -> sqlite::Result> { let mut query = String::from("SELECT id, ts, loc, cmd FROM history\n"); let mut has_cond = false; let mut cond = |c| { if has_cond { query += "AND "; } query += c; query += "\n"; has_cond = true; }; let path_prefix = path_prefix.map(|p| canonical_path(p.to_vec())); let mut upper = Vec::new(); if let Some(path_prefix) = &path_prefix { if path_strict { cond("loc = ?"); } else { upper = path_prefix.to_vec(); if let Some(last) = upper.last_mut() { *last = last.saturating_add(1); } else { upper.push(0xFF); } cond("loc >= ? AND loc < ?"); } } if min_time.is_some() { cond("ts >= ?"); } if max_time.is_some() { cond("ts <= ?"); } query += "ORDER BY id asc"; let mut stmt = self.db.prepare(&query)?; let mut i = 1; if let Some(path_prefix) = &path_prefix { stmt.bind((i, path_prefix.as_slice()))?; i += 1; if !path_strict { stmt.bind((i, upper.as_slice()))?; i += 1; } } if let Some(t) = min_time { stmt.bind((i, t.unix() as i64))?; i += 1; } if let Some(t) = max_time { stmt.bind((i, t.unix() as i64))?; i += 1; } let _ = i; Ok(std::iter::from_fn(move || match stmt.next() { Ok(State::Row) => { let ts: i64 = stmt.read::(1).ok()?; let loc: Vec = stmt.read::, _>(2).ok()?; let cmd: Vec = stmt.read::, _>(3).ok()?; let time = DateTime::from_unix(ts as u64); Some(HistoryEntry { time, loc, cmd }) } _ => None, })) } } pub fn local_history_filter( hist: Vec, min_time: Option, max_time: Option, path_prefix: Option<&[u8]>, path_strict: bool, ) -> impl Iterator { let path_prefix = path_prefix.map(|p| canonical_path(p.to_vec())); hist.into_iter().filter(move |entry| { if let Some(t) = &min_time && t.unix() > entry.time.unix() { return false; } if let Some(t) = &max_time && t.unix() < entry.time.unix() { return false; } if let Some(path_prefix) = &path_prefix { if path_strict { let canon_path = canonical_path(entry.loc.clone()); return &canon_path == path_prefix; } else { if !entry.loc.starts_with(&path_prefix) { return false; } } } true }) }