Skip to main content

smriti/db/
excluded_folder_repo.rs

1//! Per-library folder exclusions.
2
3use rusqlite::{params, Connection, Result as SqliteResult};
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct ExcludedFolderRecord {
7    pub relative_path: String,
8    pub created_at: String,
9    pub indexed_count: i64,
10}
11
12pub struct ExcludedFolderRepo<'a> {
13    conn: &'a Connection,
14}
15
16impl<'a> ExcludedFolderRepo<'a> {
17    pub fn new(conn: &'a Connection) -> Self {
18        Self { conn }
19    }
20
21    pub fn list(&self) -> SqliteResult<Vec<ExcludedFolderRecord>> {
22        let mut stmt = self.conn.prepare(
23            "SELECT e.relative_path, e.created_at,
24                    (SELECT COUNT(*) FROM photos p
25                     WHERE p.is_trashed = FALSE
26                       AND (p.file_path = e.relative_path OR p.file_path LIKE e.relative_path || '/%'))
27             FROM excluded_folders e
28             ORDER BY e.relative_path COLLATE NOCASE",
29        )?;
30        let records = stmt
31            .query_map([], |row| {
32                Ok(ExcludedFolderRecord {
33                    relative_path: row.get(0)?,
34                    created_at: row.get(1)?,
35                    indexed_count: row.get(2)?,
36                })
37            })?
38            .collect();
39        records
40    }
41
42    pub fn relative_paths(&self) -> SqliteResult<Vec<String>> {
43        let mut stmt = self
44            .conn
45            .prepare("SELECT relative_path FROM excluded_folders ORDER BY relative_path")?;
46        let paths = stmt.query_map([], |row| row.get(0))?.collect();
47        paths
48    }
49
50    pub fn count_indexed_under(&self, relative_path: &str) -> SqliteResult<i64> {
51        self.conn.query_row(
52            "SELECT COUNT(*) FROM photos
53             WHERE is_trashed = FALSE
54               AND (file_path = ?1 OR file_path LIKE ?1 || '/%')",
55            params![relative_path],
56            |row| row.get(0),
57        )
58    }
59
60    pub fn insert_and_remove_indexed(
61        &self,
62        relative_path: &str,
63    ) -> SqliteResult<ExcludedFolderRecord> {
64        let tx = self.conn.unchecked_transaction()?;
65        tx.execute(
66            "INSERT OR IGNORE INTO excluded_folders (relative_path) VALUES (?1)",
67            params![relative_path],
68        )?;
69        tx.execute(
70            "DELETE FROM photos
71             WHERE file_path = ?1 OR file_path LIKE ?1 || '/%'",
72            params![relative_path],
73        )?;
74        tx.commit()?;
75        self.get(relative_path)
76    }
77
78    pub fn remove(&self, relative_path: &str) -> SqliteResult<bool> {
79        let removed = self.conn.execute(
80            "DELETE FROM excluded_folders WHERE relative_path = ?1",
81            params![relative_path],
82        )?;
83        Ok(removed > 0)
84    }
85
86    fn get(&self, relative_path: &str) -> SqliteResult<ExcludedFolderRecord> {
87        self.conn.query_row(
88            "SELECT e.relative_path, e.created_at,
89                    (SELECT COUNT(*) FROM photos p
90                     WHERE p.is_trashed = FALSE
91                       AND (p.file_path = e.relative_path OR p.file_path LIKE e.relative_path || '/%'))
92             FROM excluded_folders e
93             WHERE e.relative_path = ?1",
94            params![relative_path],
95            |row| {
96                Ok(ExcludedFolderRecord {
97                    relative_path: row.get(0)?,
98                    created_at: row.get(1)?,
99                    indexed_count: row.get(2)?,
100                })
101            },
102        )
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use rusqlite::Connection;
110
111    #[test]
112    fn counts_and_removes_only_folder_descendants() {
113        let conn = Connection::open_in_memory().unwrap();
114        crate::db::create_schema(&conn).unwrap();
115        conn.execute(
116            "INSERT INTO photos (file_path, file_name, file_hash, file_size)
117             VALUES
118             ('Trips/Goa/a.jpg', 'a.jpg', 'h1', 12000),
119             ('Trips/Goa/Sub/b.jpg', 'b.jpg', 'h2', 12000),
120             ('Trips/Goa2/c.jpg', 'c.jpg', 'h3', 12000),
121             ('Other/Goa/d.jpg', 'd.jpg', 'h4', 12000)",
122            [],
123        )
124        .unwrap();
125
126        let repo = ExcludedFolderRepo::new(&conn);
127        assert_eq!(repo.count_indexed_under("Trips/Goa").unwrap(), 2);
128
129        let record = repo.insert_and_remove_indexed("Trips/Goa").unwrap();
130        assert_eq!(record.relative_path, "Trips/Goa");
131        assert_eq!(record.indexed_count, 0);
132
133        let remaining: i64 = conn
134            .query_row("SELECT COUNT(*) FROM photos", [], |row| row.get(0))
135            .unwrap();
136        assert_eq!(remaining, 2);
137    }
138}