From 4f725425fc0a771f3348f57de258c1048d1996be Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Tue, 12 Aug 2025 18:54:07 +0300 Subject: [PATCH] db: switch to sqlite as the primary backend Signed-off-by: NotAShelf Change-Id: I6a6a69648f81d0d094e11a3e0f0a19d3b8eccd5d --- Cargo.lock | 173 +++++++++---------------- Cargo.toml | 2 +- README.md | 4 +- src/commands/decode.rs | 4 +- src/commands/delete.rs | 4 +- src/commands/list.rs | 4 +- src/commands/query.rs | 6 +- src/commands/store.rs | 4 +- src/commands/wipe.rs | 4 +- src/db/mod.rs | 277 ++++++++++++++++++++++------------------- src/import.rs | 24 ++-- src/main.rs | 14 ++- 12 files changed, 246 insertions(+), 274 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 635a19d..3a04cad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -424,6 +424,18 @@ dependencies = [ "zune-inflate", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastrand" version = "2.3.0" @@ -456,23 +468,10 @@ dependencies = [ ] [[package]] -name = "fs2" -version = "0.4.3" +name = "foldhash" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "getrandom" @@ -522,6 +521,18 @@ name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown", +] [[package]] name = "heck" @@ -578,15 +589,6 @@ dependencies = [ "hashbrown", ] -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - [[package]] name = "interpolate_name" version = "0.2.4" @@ -685,6 +687,17 @@ dependencies = [ "libc", ] +[[package]] +name = "libsqlite3-sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -697,16 +710,6 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" -[[package]] -name = "lock_api" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" -dependencies = [ - "autocfg", - "scopeguard", -] - [[package]] name = "log" version = "0.4.27" @@ -854,31 +857,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "parking_lot" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" -dependencies = [ - "cfg-if", - "instant", - "libc", - "redox_syscall", - "smallvec", - "winapi", -] - [[package]] name = "paste" version = "1.0.15" @@ -1105,15 +1083,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "redox_users" version = "0.5.2" @@ -1182,6 +1151,20 @@ dependencies = [ "serde", ] +[[package]] +name = "rusqlite" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" +dependencies = [ + "bitflags 2.9.1", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + [[package]] name = "rustix" version = "0.38.44" @@ -1214,12 +1197,6 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - [[package]] name = "serde" version = "1.0.219" @@ -1270,22 +1247,6 @@ dependencies = [ "quote", ] -[[package]] -name = "sled" -version = "0.34.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935" -dependencies = [ - "crc32fast", - "crossbeam-epoch", - "crossbeam-utils", - "fs2", - "fxhash", - "libc", - "log", - "parking_lot", -] - [[package]] name = "smallvec" version = "1.15.1" @@ -1303,8 +1264,8 @@ dependencies = [ "image", "log", "rmp-serde", + "rusqlite", "serde", - "sled", "thiserror 2.0.14", "wl-clipboard-rs", ] @@ -1478,6 +1439,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version-compare" version = "0.2.0" @@ -1633,28 +1600,6 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows-link" version = "0.1.3" diff --git a/Cargo.toml b/Cargo.toml index 9f5fd97..3a3a176 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,6 @@ rust-version = "1.85" [dependencies] clap = { version = "4.5.44", features = ["derive"] } -sled = "0.34.7" dirs = "6.0.0" serde = { version = "1.0.219", features = ["derive"] } rmp-serde = "1.3.0" @@ -20,3 +19,4 @@ env_logger = "0.11.8" clap-verbosity-flag = "3.0.3" thiserror = "2.0.14" wl-clipboard-rs = "0.9.2" +rusqlite = {version = "0.37.0", features = [ "bundled" ] } diff --git a/README.md b/README.md index 555d4e8..88c144f 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ must handle the migration yourself, with one simple command. ```bash $ cliphist list --db ~/.cache/cliphist/db | stash --import-tsv -# > Imported 750 records from TSV into sled database. +# > Imported 750 records from TSV into database. ``` Alternatively, you may first export from Cliphist and _then_ import the @@ -88,5 +88,5 @@ database. ```bash $ cliphist list --db ~/.cache/cliphist/db > cliphist.tsv $ stash --import-tsv < cliphist.tsv -# > Imported 750 records from TSV into sled database. +# > Imported 750 records from TSV into database. ``` diff --git a/src/commands/decode.rs b/src/commands/decode.rs index b1c94e8..b545f53 100644 --- a/src/commands/decode.rs +++ b/src/commands/decode.rs @@ -1,4 +1,4 @@ -use crate::db::{ClipboardDb, SledClipboardDb}; +use crate::db::{ClipboardDb, SqliteClipboardDb}; use std::io::{Read, Write}; @@ -13,7 +13,7 @@ pub trait DecodeCommand { ) -> Result<(), StashError>; } -impl DecodeCommand for SledClipboardDb { +impl DecodeCommand for SqliteClipboardDb { fn decode( &self, in_: impl Read, diff --git a/src/commands/delete.rs b/src/commands/delete.rs index bd5d5c7..64fd1cf 100644 --- a/src/commands/delete.rs +++ b/src/commands/delete.rs @@ -1,4 +1,4 @@ -use crate::db::{ClipboardDb, SledClipboardDb, StashError}; +use crate::db::{ClipboardDb, SqliteClipboardDb, StashError}; use std::io::Read; @@ -6,7 +6,7 @@ pub trait DeleteCommand { fn delete(&self, input: impl Read) -> Result; } -impl DeleteCommand for SledClipboardDb { +impl DeleteCommand for SqliteClipboardDb { fn delete(&self, input: impl Read) -> Result { match self.delete_entries(input) { Ok(deleted) => { diff --git a/src/commands/list.rs b/src/commands/list.rs index 28d42a8..f79f407 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -1,11 +1,11 @@ -use crate::db::{ClipboardDb, SledClipboardDb}; +use crate::db::{ClipboardDb, SqliteClipboardDb}; use std::io::Write; pub trait ListCommand { fn list(&self, out: impl Write, preview_width: u32) -> Result<(), crate::db::StashError>; } -impl ListCommand for SledClipboardDb { +impl ListCommand for SqliteClipboardDb { fn list(&self, out: impl Write, preview_width: u32) -> Result<(), crate::db::StashError> { self.list_entries(out, preview_width)?; log::info!("Listed clipboard entries"); diff --git a/src/commands/query.rs b/src/commands/query.rs index 981334f..6673648 100644 --- a/src/commands/query.rs +++ b/src/commands/query.rs @@ -1,4 +1,4 @@ -use crate::db::{ClipboardDb, SledClipboardDb}; +use crate::db::{ClipboardDb, SqliteClipboardDb}; use crate::db::StashError; @@ -6,8 +6,8 @@ pub trait QueryCommand { fn query_delete(&self, query: &str) -> Result; } -impl QueryCommand for SledClipboardDb { +impl QueryCommand for SqliteClipboardDb { fn query_delete(&self, query: &str) -> Result { - ::delete_query(self, query) + ::delete_query(self, query) } } diff --git a/src/commands/store.rs b/src/commands/store.rs index 40db4d0..4e2c769 100644 --- a/src/commands/store.rs +++ b/src/commands/store.rs @@ -1,4 +1,4 @@ -use crate::db::{ClipboardDb, SledClipboardDb}; +use crate::db::{ClipboardDb, SqliteClipboardDb}; use std::io::Read; @@ -12,7 +12,7 @@ pub trait StoreCommand { ) -> Result<(), crate::db::StashError>; } -impl StoreCommand for SledClipboardDb { +impl StoreCommand for SqliteClipboardDb { fn store( &self, input: impl Read, diff --git a/src/commands/wipe.rs b/src/commands/wipe.rs index cfde239..d815527 100644 --- a/src/commands/wipe.rs +++ b/src/commands/wipe.rs @@ -1,4 +1,4 @@ -use crate::db::{ClipboardDb, SledClipboardDb}; +use crate::db::{ClipboardDb, SqliteClipboardDb}; use crate::db::StashError; @@ -6,7 +6,7 @@ pub trait WipeCommand { fn wipe(&self) -> Result<(), StashError>; } -impl WipeCommand for SledClipboardDb { +impl WipeCommand for SqliteClipboardDb { fn wipe(&self) -> Result<(), StashError> { self.wipe_db()?; log::info!("Database wiped"); diff --git a/src/db/mod.rs b/src/db/mod.rs index 598d70b..1beafbf 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -4,9 +4,9 @@ use std::str; use image::{GenericImageView, ImageFormat}; use log::{error, info}; -use rmp_serde::{decode::from_read, encode::to_vec}; + +use rusqlite::{Connection, OptionalExtension, params}; use serde::{Deserialize, Serialize}; -use sled::{Db, IVec}; use thiserror::Error; #[derive(Error, Debug)] @@ -15,8 +15,7 @@ pub enum StashError { EmptyOrTooLarge, #[error("Input is all whitespace, skipping store.")] AllWhitespace, - #[error("Failed to serialize entry: {0}")] - Serialize(String), + #[error("Failed to store entry: {0}")] Store(String), #[error("Error reading entry during deduplication: {0}")] @@ -41,10 +40,7 @@ pub enum StashError { DecodeExtractId(String), #[error("Failed to get entry for decode: {0}")] DecodeGet(String), - #[error("No entry found for id {0}")] - DecodeNoEntry(u64), - #[error("Failed to decode entry: {0}")] - DecodeDecode(String), + #[error("Failed to write decoded entry: {0}")] DecodeWrite(String), #[error("Failed to delete entry during query delete: {0}")] @@ -89,11 +85,25 @@ impl fmt::Display for Entry { } } -pub struct SledClipboardDb { - pub db: Db, +pub struct SqliteClipboardDb { + pub conn: Connection, } -impl ClipboardDb for SledClipboardDb { +impl SqliteClipboardDb { + pub fn new(conn: Connection) -> Result { + conn.execute_batch( + "CREATE TABLE IF NOT EXISTS clipboard ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + contents BLOB NOT NULL, + mime TEXT + );", + ) + .map_err(|e| StashError::Store(e.to_string()))?; + Ok(Self { conn }) + } +} + +impl ClipboardDb for SqliteClipboardDb { fn store_entry( &self, mut input: impl Read, @@ -112,98 +122,111 @@ impl ClipboardDb for SledClipboardDb { self.deduplicate(&buf, max_dedupe_search)?; - let entry = Entry { - contents: buf.clone(), - mime, - }; - - let id = self.next_sequence(); - let enc = to_vec(&entry).map_err(|e| StashError::Serialize(e.to_string()))?; - - self.db - .insert(u64_to_ivec(id), enc) + self.conn + .execute( + "INSERT INTO clipboard (contents, mime) VALUES (?1, ?2)", + params![buf, mime], + ) .map_err(|e| StashError::Store(e.to_string()))?; + self.trim_db(max_items)?; - Ok(id) + Ok(self.next_sequence()) } fn deduplicate(&self, buf: &[u8], max: u64) -> Result { - let mut count = 0; + let mut stmt = self + .conn + .prepare("SELECT id, contents FROM clipboard ORDER BY id DESC LIMIT ?1") + .map_err(|e| StashError::DeduplicationRead(e.to_string()))?; + let mut rows = stmt + .query(params![i64::try_from(max).unwrap_or(i64::MAX)]) + .map_err(|e| StashError::DeduplicationRead(e.to_string()))?; let mut deduped = 0; - for item in self - .db - .iter() - .rev() - .take(usize::try_from(max).unwrap_or(usize::MAX)) + while let Some(row) = rows + .next() + .map_err(|e| StashError::DeduplicationRead(e.to_string()))? { - let (k, v) = match item { - Ok((k, v)) => (k, v), - Err(e) => return Err(StashError::DeduplicationRead(e.to_string())), - }; - let entry: Entry = match from_read(v.as_ref()) { - Ok(e) => e, - Err(e) => return Err(StashError::DeduplicationDecode(e.to_string())), - }; - if entry.contents == buf { - self.db - .remove(k) - .map(|_| { - deduped += 1; - }) + let id: u64 = row + .get(0) + .map_err(|e| StashError::DeduplicationDecode(e.to_string()))?; + let contents: Vec = row + .get(1) + .map_err(|e| StashError::DeduplicationDecode(e.to_string()))?; + if contents == buf { + self.conn + .execute("DELETE FROM clipboard WHERE id = ?1", params![id]) .map_err(|e| StashError::DeduplicationRemove(e.to_string()))?; - } - count += 1; - if count >= max { - break; + deduped += 1; } } Ok(deduped) } fn trim_db(&self, max: u64) -> Result<(), StashError> { - let mut keys: Vec<_> = self - .db - .iter() - .rev() - .filter_map(|kv| match kv { - Ok((k, _)) => Some(k), - Err(_e) => None, - }) - .collect(); - if keys.len() as u64 > max { - for k in keys.drain(usize::try_from(max).unwrap_or(0)..) { - self.db - .remove(k) - .map_err(|e| StashError::Trim(e.to_string()))?; - } + let count: u64 = self + .conn + .query_row("SELECT COUNT(*) FROM clipboard", [], |row| row.get(0)) + .map_err(|e| StashError::Trim(e.to_string()))?; + if count > max { + let to_delete = count - max; + self.conn.execute( + "DELETE FROM clipboard WHERE id IN (SELECT id FROM clipboard ORDER BY id ASC LIMIT ?1)", + params![i64::try_from(to_delete).unwrap_or(i64::MAX)], + ).map_err(|e| StashError::Trim(e.to_string()))?; } Ok(()) } fn delete_last(&self) -> Result<(), StashError> { - if let Some((k, _)) = self.db.iter().next_back().and_then(Result::ok) { - self.db - .remove(k) - .map(|_| ()) - .map_err(|e| StashError::DeleteLast(e.to_string())) + let id: Option = self + .conn + .query_row( + "SELECT id FROM clipboard ORDER BY id DESC LIMIT 1", + [], + |row| row.get(0), + ) + .optional() + .map_err(|e| StashError::DeleteLast(e.to_string()))?; + if let Some(id) = id { + self.conn + .execute("DELETE FROM clipboard WHERE id = ?1", params![id]) + .map_err(|e| StashError::DeleteLast(e.to_string()))?; + Ok(()) } else { Err(StashError::NoEntriesToDelete) } } fn wipe_db(&self) -> Result<(), StashError> { - self.db.clear().map_err(|e| StashError::Wipe(e.to_string())) + self.conn + .execute("DELETE FROM clipboard", []) + .map_err(|e| StashError::Wipe(e.to_string()))?; + Ok(()) } fn list_entries(&self, mut out: impl Write, preview_width: u32) -> Result { + let mut stmt = self + .conn + .prepare("SELECT id, contents, mime FROM clipboard ORDER BY id DESC") + .map_err(|e| StashError::ListDecode(e.to_string()))?; + let mut rows = stmt + .query([]) + .map_err(|e| StashError::ListDecode(e.to_string()))?; let mut listed = 0; - for (k, v) in self.db.iter().rev().filter_map(Result::ok) { - let id = ivec_to_u64(&k); - let entry: Entry = match from_read(v.as_ref()) { - Ok(e) => e, - Err(e) => return Err(StashError::ListDecode(e.to_string())), - }; - let preview = preview_entry(&entry.contents, entry.mime.as_deref(), preview_width); + while let Some(row) = rows + .next() + .map_err(|e| StashError::ListDecode(e.to_string()))? + { + let id: u64 = row + .get(0) + .map_err(|e| StashError::ListDecode(e.to_string()))?; + let contents: Vec = row + .get(1) + .map_err(|e| StashError::ListDecode(e.to_string()))?; + let mime: Option = row + .get(2) + .map_err(|e| StashError::ListDecode(e.to_string()))?; + let preview = preview_entry(&contents, mime.as_deref(), preview_width); if writeln!(out, "{id}\t{preview}").is_ok() { listed += 1; } @@ -226,38 +249,44 @@ impl ClipboardDb for SledClipboardDb { buf }; let id = extract_id(&s).map_err(|e| StashError::DecodeExtractId(e.to_string()))?; - let v = self - .db - .get(u64_to_ivec(id)) - .map_err(|e| StashError::DecodeGet(e.to_string()))? - .ok_or(StashError::DecodeNoEntry(id))?; - let entry: Entry = - from_read(v.as_ref()).map_err(|e| StashError::DecodeDecode(e.to_string()))?; - - out.write_all(&entry.contents) + let (contents, _mime): (Vec, Option) = self + .conn + .query_row( + "SELECT contents, mime FROM clipboard WHERE id = ?1", + params![id], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .map_err(|e| StashError::DecodeGet(e.to_string()))?; + out.write_all(&contents) .map_err(|e| StashError::DecodeWrite(e.to_string()))?; info!("Decoded entry with id {id}"); Ok(()) } fn delete_query(&self, query: &str) -> Result { + let mut stmt = self + .conn + .prepare("SELECT id, contents FROM clipboard") + .map_err(|e| StashError::QueryDelete(e.to_string()))?; + let mut rows = stmt + .query([]) + .map_err(|e| StashError::QueryDelete(e.to_string()))?; let mut deleted = 0; - for (k, v) in self.db.iter().filter_map(Result::ok) { - let entry: Entry = match from_read(v.as_ref()) { - Ok(e) => e, - Err(_) => continue, - }; - if entry - .contents - .windows(query.len()) - .any(|w| w == query.as_bytes()) - { - self.db - .remove(k) - .map(|_| { - deleted += 1; - }) + while let Some(row) = rows + .next() + .map_err(|e| StashError::QueryDelete(e.to_string()))? + { + let id: u64 = row + .get(0) + .map_err(|e| StashError::QueryDelete(e.to_string()))?; + let contents: Vec = row + .get(1) + .map_err(|e| StashError::QueryDelete(e.to_string()))?; + if contents.windows(query.len()).any(|w| w == query.as_bytes()) { + self.conn + .execute("DELETE FROM clipboard WHERE id = ?1", params![id]) .map_err(|e| StashError::QueryDelete(e.to_string()))?; + deleted += 1; } } Ok(deleted) @@ -268,25 +297,24 @@ impl ClipboardDb for SledClipboardDb { let mut deleted = 0; for line in reader.lines().map_while(Result::ok) { if let Ok(id) = extract_id(&line) { - self.db - .remove(u64_to_ivec(id)) - .map(|_| { - deleted += 1; - }) + self.conn + .execute("DELETE FROM clipboard WHERE id = ?1", params![id]) .map_err(|e| StashError::DeleteEntry(id, e.to_string()))?; + deleted += 1; } } Ok(deleted) } fn next_sequence(&self) -> u64 { - let last = self - .db - .iter() - .next_back() - .and_then(std::result::Result::ok) - .map(|(k, _)| ivec_to_u64(&k)); - last.unwrap_or(0) + 1 + match self + .conn + .query_row("SELECT MAX(id) FROM clipboard", [], |row| { + row.get::<_, Option>(0) + }) { + Ok(Some(max_id)) => max_id + 1, + Ok(None) | Err(_) => 1, + } } } @@ -296,20 +324,6 @@ pub fn extract_id(input: &str) -> Result { id_str.parse().map_err(|_| "invalid id") } -pub fn u64_to_ivec(v: u64) -> IVec { - IVec::from(&v.to_be_bytes()[..]) -} - -pub fn ivec_to_u64(v: &IVec) -> u64 { - let arr: [u8; 8] = if let Ok(arr) = v.as_ref().try_into() { - arr - } else { - error!("Failed to convert IVec to u64: invalid length"); - return 0; - }; - u64::from_be_bytes(arr) -} - pub fn detect_mime(data: &[u8]) -> Option { if image::guess_format(data).is_ok() { match image::guess_format(data) { @@ -347,7 +361,13 @@ pub fn preview_entry(data: &[u8], mime: Option<&str>, width: u32) -> String { ); } } else if mime == "application/json" || mime.starts_with("text/") { - let s = str::from_utf8(data).unwrap_or(""); + let s = match str::from_utf8(data) { + Ok(s) => s, + Err(e) => { + error!("Failed to decode UTF-8 clipboard data: {e}"); + "" + } + }; let s = s.trim().replace(|c: char| c.is_whitespace(), " "); return truncate(&s, width as usize, "…"); } @@ -366,7 +386,12 @@ pub fn truncate(s: &str, max: usize, ellip: &str) -> String { pub fn size_str(size: usize) -> String { let units = ["B", "KiB", "MiB"]; - let mut fsize = f64::from(u32::try_from(size).unwrap_or(u32::MAX)); + let mut fsize = if let Ok(val) = u32::try_from(size) { + f64::from(val) + } else { + error!("Clipboard entry size too large for display: {size}"); + f64::from(u32::MAX) + }; let mut i = 0; while fsize >= 1024.0 && i < units.len() - 1 { fsize /= 1024.0; diff --git a/src/import.rs b/src/import.rs index 2276489..c60d7d6 100644 --- a/src/import.rs +++ b/src/import.rs @@ -1,4 +1,4 @@ -use crate::db::{Entry, SledClipboardDb, detect_mime, u64_to_ivec}; +use crate::db::{Entry, SqliteClipboardDb, detect_mime}; use log::{error, info}; use std::io::{self, BufRead}; @@ -6,31 +6,27 @@ pub trait ImportCommand { fn import_tsv(&self, input: impl io::Read); } -impl ImportCommand for SledClipboardDb { +impl ImportCommand for SqliteClipboardDb { fn import_tsv(&self, input: impl io::Read) { let reader = io::BufReader::new(input); let mut imported = 0; for line in reader.lines().map_while(Result::ok) { let mut parts = line.splitn(2, '\t'); if let (Some(id_str), Some(val)) = (parts.next(), parts.next()) { - if let Ok(id) = id_str.parse::() { + if let Ok(_id) = id_str.parse::() { let entry = Entry { contents: val.as_bytes().to_vec(), mime: detect_mime(val.as_bytes()), }; - let enc = match rmp_serde::encode::to_vec(&entry) { - Ok(enc) => enc, - Err(e) => { - error!("Failed to encode entry for id {id}: {e}"); - continue; - } - }; - match self.db.insert(u64_to_ivec(id), enc) { + match self.conn.execute( + "INSERT INTO clipboard (contents, mime) VALUES (?1, ?2)", + rusqlite::params![entry.contents, entry.mime], + ) { Ok(_) => { imported += 1; - info!("Imported entry with id {id}"); + info!("Imported entry from TSV"); } - Err(e) => error!("Failed to insert entry with id {id}: {e}"), + Err(e) => error!("Failed to insert entry: {e}"), } } else { error!("Failed to parse id from line: {id_str}"); @@ -39,6 +35,6 @@ impl ImportCommand for SledClipboardDb { error!("Malformed TSV line: {line:?}"); } } - info!("Imported {imported} records from TSV into sled database."); + info!("Imported {imported} records from TSV into SQLite database."); } } diff --git a/src/main.rs b/src/main.rs index 73ddb27..dcb600f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -95,7 +95,7 @@ fn report_error(result: Result, context: &str) -> } /// Watch clipboard and store changes -fn run_daemon(db: &db::SledClipboardDb, max_dedupe_search: u64, max_items: u64) { +fn run_daemon(db: &db::SqliteClipboardDb, max_dedupe_search: u64, max_items: u64) { log::info!("Starting clipboard watch daemon (Wayland)"); let mut last_contents: Option> = None; @@ -149,12 +149,18 @@ fn main() { .join("db") }); - let sled_db = sled::open(&db_path).unwrap_or_else(|e| { - log::error!("Failed to open database: {e}"); + let conn = rusqlite::Connection::open(&db_path).unwrap_or_else(|e| { + log::error!("Failed to open SQLite database: {e}"); process::exit(1); }); - let db = db::SledClipboardDb { db: sled_db }; + let db = match db::SqliteClipboardDb::new(conn) { + Ok(db) => db, + Err(e) => { + log::error!("Failed to initialize SQLite database: {e}"); + process::exit(1); + } + }; match cli.command { Some(Command::Store) => {