use anyhow::Result; use rusqlite::Connection; use std::path::PathBuf; fn db_path() -> PathBuf { let dir = dirs_or_default(); std::fs::create_dir_all(&dir).ok(); dir.join("ignores.db") } fn dirs_or_default() -> PathBuf { std::env::var("DEDUPER_DB_DIR") .map(PathBuf::from) .unwrap_or_else(|_| { let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()); PathBuf::from(home).join(".config").join("deduper") }) } pub fn open_db() -> Result { let path = db_path(); let conn = Connection::open(&path)?; conn.execute_batch( "CREATE TABLE IF NOT EXISTS ignored_groups ( fingerprint TEXT PRIMARY KEY, created_at TEXT DEFAULT (datetime('now')), note TEXT DEFAULT '' );", )?; Ok(conn) } /// Fingerprint = sorted sha256 hashes joined by `|` pub fn group_fingerprint(sha256s: &[&str]) -> String { let mut sorted: Vec<&str> = sha256s.to_vec(); sorted.sort(); sorted.dedup(); sorted.join("|") } pub fn ignore_group(conn: &Connection, fingerprint: &str) -> Result<()> { conn.execute( "INSERT OR IGNORE INTO ignored_groups (fingerprint) VALUES (?1)", [fingerprint], )?; Ok(()) } pub fn is_group_ignored(conn: &Connection, fingerprint: &str) -> bool { conn.query_row( "SELECT 1 FROM ignored_groups WHERE fingerprint = ?1", [fingerprint], |_| Ok(true), ) .unwrap_or(false) } pub fn remove_ignore(conn: &Connection, fingerprint: &str) -> Result<()> { conn.execute( "DELETE FROM ignored_groups WHERE fingerprint = ?1", [fingerprint], )?; Ok(()) } pub fn list_ignored(conn: &Connection) -> Result> { let mut stmt = conn.prepare("SELECT fingerprint, created_at FROM ignored_groups ORDER BY created_at DESC")?; let rows = stmt.query_map([], |row| { Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)) })?; Ok(rows.filter_map(|r| r.ok()).collect()) } pub fn open_db_in_memory() -> Result { let conn = Connection::open_in_memory()?; conn.execute_batch( "CREATE TABLE IF NOT EXISTS ignored_groups ( fingerprint TEXT PRIMARY KEY, created_at TEXT DEFAULT (datetime('now')), note TEXT DEFAULT '' );", )?; Ok(conn) } #[cfg(test)] mod tests { use super::*; #[test] fn ignore_and_check_group() { let conn = open_db_in_memory().unwrap(); let fp = group_fingerprint(&["sha_b", "sha_a"]); assert!(!is_group_ignored(&conn, &fp)); ignore_group(&conn, &fp).unwrap(); assert!(is_group_ignored(&conn, &fp)); } #[test] fn fingerprint_is_sorted_and_stable() { let fp1 = group_fingerprint(&["bbb", "aaa"]); let fp2 = group_fingerprint(&["aaa", "bbb"]); assert_eq!(fp1, fp2); assert_eq!(fp1, "aaa|bbb"); } #[test] fn remove_ignore_works() { let conn = open_db_in_memory().unwrap(); let fp = group_fingerprint(&["x", "y"]); ignore_group(&conn, &fp).unwrap(); assert!(is_group_ignored(&conn, &fp)); remove_ignore(&conn, &fp).unwrap(); assert!(!is_group_ignored(&conn, &fp)); } #[test] fn list_ignored_returns_entries() { let conn = open_db_in_memory().unwrap(); ignore_group(&conn, "fp1").unwrap(); ignore_group(&conn, "fp2").unwrap(); let list = list_ignored(&conn).unwrap(); assert_eq!(list.len(), 2); } }