feat: "Not a dupe" ignore with SQLite persistence
- New ignore_db module with SQLite-backed dismissal storage - Groups flagged as not-a-dupe are persisted to ~/.config/deduper/ignores.db - Fingerprint based on sorted SHA256 hashes (content-stable) - Ignored groups filtered out on subsequent runs - Review UI: green "Not a dupe" button per group - Dismissed groups fade out immediately in browser - DEDUPER_DB_DIR env var to override DB location - 4 new unit tests for ignore_db - 29 tests passing
This commit is contained in:
125
src/ignore_db.rs
Normal file
125
src/ignore_db.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
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<Connection> {
|
||||
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<Vec<(String, String)>> {
|
||||
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<Connection> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user