use anyhow::{Context, Result};
use rusqlite::TransactionBehavior;
use super::revision::bump_revision_tx;
use crate::db::sqlite_helpers::collect_rows;
use crate::db::types::{ArchiveChangeKind, CapturePolicy, CaptureSettings, Database};
impl Database {
pub(crate) fn capture_settings(&self) -> Result<CaptureSettings> {
self.conn
.query_row(
"SELECT paused, retention_seconds, api_key_filter_enabled, ocr_enabled FROM clipmem_settings WHERE id = 1",
[],
|row| {
let paused = row.get::<_, i64>(0)? != 0;
let retention_seconds = row
.get::<_, Option<i64>>(1)?
.map(u64::try_from)
.transpose()
.map_err(|source| {
rusqlite::Error::FromSqlConversionFailure(
1,
rusqlite::types::Type::Integer,
Box::new(source),
)
})?;
let api_key_filter_enabled = row.get::<_, i64>(2)? != 0;
let ocr_enabled = row.get::<_, i64>(3)? != 0;
Ok(CaptureSettings::new(
paused,
retention_seconds,
api_key_filter_enabled,
ocr_enabled,
))
},
)
.context("load capture settings")
}
pub(crate) fn capture_policy(&self) -> Result<CapturePolicy> {
Ok(CapturePolicy::new(
self.capture_settings()?,
self.list_ignored_bundle_ids()?,
))
}
pub(crate) fn set_paused(&mut self, paused: bool) -> Result<CaptureSettings> {
let tx = self
.conn
.transaction_with_behavior(TransactionBehavior::Immediate)
.context("begin paused setting transaction")?;
let changed = tx
.execute(
"UPDATE clipmem_settings SET paused = ?1 WHERE id = 1 AND paused != ?1",
[if paused { 1_i64 } else { 0_i64 }],
)
.context("update paused setting")?;
if changed != 0 {
bump_revision_tx(&tx, &[ArchiveChangeKind::Settings])?;
}
tx.commit().context("commit paused setting transaction")?;
self.capture_settings()
}
pub(crate) fn set_retention_seconds(
&mut self,
retention_seconds: Option<u64>,
) -> Result<CaptureSettings> {
let retention_seconds = retention_seconds
.map(i64::try_from)
.transpose()
.context("retention exceeds SQLite INTEGER range")?;
let tx = self
.conn
.transaction_with_behavior(TransactionBehavior::Immediate)
.context("begin retention setting transaction")?;
let changed = tx
.execute(
"UPDATE clipmem_settings SET retention_seconds = ?1 WHERE id = 1 AND COALESCE(retention_seconds, -1) != COALESCE(?1, -1)",
[retention_seconds],
)
.context("update retention setting")?;
if changed != 0 {
bump_revision_tx(&tx, &[ArchiveChangeKind::Settings])?;
}
tx.commit()
.context("commit retention setting transaction")?;
self.capture_settings()
}
pub(crate) fn set_api_key_filter_enabled(&mut self, enabled: bool) -> Result<CaptureSettings> {
let tx = self
.conn
.transaction_with_behavior(TransactionBehavior::Immediate)
.context("begin api key filter setting transaction")?;
let changed = tx
.execute(
"UPDATE clipmem_settings SET api_key_filter_enabled = ?1 WHERE id = 1 AND api_key_filter_enabled != ?1",
[if enabled { 1_i64 } else { 0_i64 }],
)
.context("update api key filter setting")?;
if changed != 0 {
bump_revision_tx(&tx, &[ArchiveChangeKind::Settings])?;
}
tx.commit()
.context("commit api key filter setting transaction")?;
self.capture_settings()
}
pub(crate) fn set_ocr_enabled(&mut self, enabled: bool) -> Result<CaptureSettings> {
let tx = self
.conn
.transaction_with_behavior(TransactionBehavior::Immediate)
.context("begin ocr setting transaction")?;
let changed = tx
.execute(
"UPDATE clipmem_settings SET ocr_enabled = ?1 WHERE id = 1 AND ocr_enabled != ?1",
[if enabled { 1_i64 } else { 0_i64 }],
)
.context("update ocr setting")?;
if changed != 0 {
bump_revision_tx(&tx, &[ArchiveChangeKind::Settings])?;
}
tx.commit().context("commit ocr setting transaction")?;
self.capture_settings()
}
pub(crate) fn reset_capture_policy(&mut self) -> Result<CapturePolicy> {
let tx = self
.conn
.transaction_with_behavior(TransactionBehavior::Immediate)
.context("begin settings reset transaction")?;
let settings_changed = tx
.execute(
r"
UPDATE clipmem_settings
SET paused = 0,
retention_seconds = NULL,
api_key_filter_enabled = 0,
ocr_enabled = 0
WHERE id = 1
AND (
paused != 0
OR retention_seconds IS NOT NULL
OR api_key_filter_enabled != 0
OR ocr_enabled != 0
)
",
[],
)
.context("reset capture settings")?;
let ignored_changed = tx
.execute("DELETE FROM ignored_bundle_ids", [])
.context("clear ignored bundle ids")?;
if settings_changed != 0 || ignored_changed != 0 {
bump_revision_tx(&tx, &[ArchiveChangeKind::Settings])?;
}
tx.commit().context("commit settings reset transaction")?;
self.capture_policy()
}
pub(crate) fn list_ignored_bundle_ids(&self) -> Result<Vec<String>> {
let mut stmt = self
.conn
.prepare("SELECT bundle_id FROM ignored_bundle_ids ORDER BY bundle_id ASC")
.context("prepare ignored bundle-id query")?;
let rows = stmt
.query_map([], |row| row.get::<_, String>(0))
.context("execute ignored bundle-id query")?;
collect_rows(rows).context("collect ignored bundle ids")
}
pub(crate) fn add_ignored_bundle_id(&mut self, bundle_id: &str) -> Result<bool> {
let bundle_id = normalize_bundle_id(bundle_id)?;
let tx = self
.conn
.transaction_with_behavior(TransactionBehavior::Immediate)
.context("begin ignored bundle-id add transaction")?;
let changed = tx
.execute(
"INSERT OR IGNORE INTO ignored_bundle_ids (bundle_id) VALUES (?1)",
[bundle_id],
)
.context("insert ignored bundle id")?;
if changed != 0 {
bump_revision_tx(&tx, &[ArchiveChangeKind::Settings])?;
}
tx.commit()
.context("commit ignored bundle-id add transaction")?;
Ok(changed != 0)
}
pub(crate) fn remove_ignored_bundle_id(&mut self, bundle_id: &str) -> Result<bool> {
let bundle_id = normalize_bundle_id(bundle_id)?;
let tx = self
.conn
.transaction_with_behavior(TransactionBehavior::Immediate)
.context("begin ignored bundle-id remove transaction")?;
let changed = tx
.execute(
"DELETE FROM ignored_bundle_ids WHERE bundle_id = ?1",
[bundle_id],
)
.context("delete ignored bundle id")?;
if changed != 0 {
bump_revision_tx(&tx, &[ArchiveChangeKind::Settings])?;
}
tx.commit()
.context("commit ignored bundle-id remove transaction")?;
Ok(changed != 0)
}
}
fn normalize_bundle_id(bundle_id: &str) -> Result<String> {
let normalized = bundle_id.trim().to_ascii_lowercase();
if normalized.is_empty() {
anyhow::bail!("bundle id cannot be empty");
}
Ok(normalized)
}