diff --git a/Cargo.lock b/Cargo.lock index bab6c3e..c2c5b62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1210,6 +1210,7 @@ dependencies = [ "rmp-serde", "serde", "sled", + "thiserror 2.0.14", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 4bfd86a..0ea81ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,3 +18,4 @@ image = "0.25.6" log = "0.4.27" env_logger = "0.11.8" clap-verbosity-flag = "3.0.3" +thiserror = "2.0.14" diff --git a/src/commands/decode.rs b/src/commands/decode.rs index 918bd66..b1c94e8 100644 --- a/src/commands/decode.rs +++ b/src/commands/decode.rs @@ -2,13 +2,26 @@ use crate::db::{ClipboardDb, SledClipboardDb}; use std::io::{Read, Write}; +use crate::db::StashError; + pub trait DecodeCommand { - fn decode(&self, in_: impl Read, out: impl Write, input: Option); + fn decode( + &self, + in_: impl Read, + out: impl Write, + input: Option, + ) -> Result<(), StashError>; } impl DecodeCommand for SledClipboardDb { - fn decode(&self, in_: impl Read, out: impl Write, input: Option) { - self.decode_entry(in_, out, input); + fn decode( + &self, + in_: impl Read, + out: impl Write, + input: Option, + ) -> Result<(), StashError> { + self.decode_entry(in_, out, input)?; log::info!("Entry decoded"); + Ok(()) } } diff --git a/src/commands/delete.rs b/src/commands/delete.rs index 39d36a8..a2822ad 100644 --- a/src/commands/delete.rs +++ b/src/commands/delete.rs @@ -1,14 +1,22 @@ -use crate::db::{ClipboardDb, SledClipboardDb}; +use crate::db::{ClipboardDb, SledClipboardDb, StashError}; use std::io::Read; pub trait DeleteCommand { - fn delete(&self, input: impl Read); + fn delete(&self, input: impl Read) -> Result; } impl DeleteCommand for SledClipboardDb { - fn delete(&self, input: impl Read) { - self.delete_entries(input); - log::info!("Entries deleted"); + fn delete(&self, input: impl Read) -> Result { + match self.delete_entries(input) { + Ok(deleted) => { + log::info!("Deleted {} entries", deleted); + Ok(deleted) + } + Err(e) => { + log::error!("Failed to delete entries: {}", e); + Err(e) + } + } } } diff --git a/src/commands/list.rs b/src/commands/list.rs index 658c34c..28d42a8 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -2,12 +2,13 @@ use crate::db::{ClipboardDb, SledClipboardDb}; use std::io::Write; pub trait ListCommand { - fn list(&self, out: impl Write, preview_width: u32); + fn list(&self, out: impl Write, preview_width: u32) -> Result<(), crate::db::StashError>; } impl ListCommand for SledClipboardDb { - fn list(&self, out: impl Write, preview_width: u32) { - self.list_entries(out, preview_width); - log::info!("Entries listed"); + fn list(&self, out: impl Write, preview_width: u32) -> Result<(), crate::db::StashError> { + self.list_entries(out, preview_width)?; + log::info!("Listed clipboard entries"); + Ok(()) } } diff --git a/src/commands/query.rs b/src/commands/query.rs index 7906462..981334f 100644 --- a/src/commands/query.rs +++ b/src/commands/query.rs @@ -1,12 +1,13 @@ use crate::db::{ClipboardDb, SledClipboardDb}; +use crate::db::StashError; + pub trait QueryCommand { - fn query_delete(&self, query: &str); + fn query_delete(&self, query: &str) -> Result; } impl QueryCommand for SledClipboardDb { - fn query_delete(&self, query: &str) { - ::delete_query(self, query); - log::info!("Entries matching query '{}' deleted", query); + fn query_delete(&self, query: &str) -> Result { + ::delete_query(self, query) } } diff --git a/src/commands/store.rs b/src/commands/store.rs index 4083e93..40db4d0 100644 --- a/src/commands/store.rs +++ b/src/commands/store.rs @@ -9,7 +9,7 @@ pub trait StoreCommand { max_dedupe_search: u64, max_items: u64, state: Option, - ); + ) -> Result<(), crate::db::StashError>; } impl StoreCommand for SledClipboardDb { @@ -19,13 +19,14 @@ impl StoreCommand for SledClipboardDb { max_dedupe_search: u64, max_items: u64, state: Option, - ) { + ) -> Result<(), crate::db::StashError> { if let Some("sensitive" | "clear") = state.as_deref() { - self.delete_last(); + self.delete_last()?; log::info!("Entry deleted"); } else { - self.store_entry(input, max_dedupe_search, max_items); + self.store_entry(input, max_dedupe_search, max_items)?; log::info!("Entry stored"); } + Ok(()) } } diff --git a/src/commands/wipe.rs b/src/commands/wipe.rs index 894c3c2..cfde239 100644 --- a/src/commands/wipe.rs +++ b/src/commands/wipe.rs @@ -1,12 +1,15 @@ use crate::db::{ClipboardDb, SledClipboardDb}; +use crate::db::StashError; + pub trait WipeCommand { - fn wipe(&self); + fn wipe(&self) -> Result<(), StashError>; } impl WipeCommand for SledClipboardDb { - fn wipe(&self) { - self.wipe_db(); + fn wipe(&self) -> Result<(), StashError> { + self.wipe_db()?; log::info!("Database wiped"); + Ok(()) } } diff --git a/src/db/mod.rs b/src/db/mod.rs index 66c0205..82156fb 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -3,21 +3,76 @@ use std::io::{BufRead, BufReader, Read, Write}; use std::str; use image::{GenericImageView, ImageFormat}; -use log::{error, info, warn}; +use log::{error, info}; use rmp_serde::{decode::from_read, encode::to_vec}; use serde::{Deserialize, Serialize}; use sled::{Db, IVec}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum StashError { + #[error("Input is empty or too large, skipping store.")] + 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}")] + DeduplicationRead(String), + #[error("Error decoding entry during deduplication: {0}")] + DeduplicationDecode(String), + #[error("Failed to remove entry during deduplication: {0}")] + DeduplicationRemove(String), + #[error("Failed to trim entry: {0}")] + Trim(String), + #[error("No entries to delete")] + NoEntriesToDelete, + #[error("Failed to delete last entry: {0}")] + DeleteLast(String), + #[error("Failed to wipe database: {0}")] + Wipe(String), + #[error("Failed to decode entry during list: {0}")] + ListDecode(String), + #[error("Failed to read input for decode: {0}")] + DecodeRead(String), + #[error("Failed to extract id for decode: {0}")] + 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}")] + QueryDelete(String), + #[error("Failed to delete entry with id {0}: {1}")] + DeleteEntry(u64, String), +} pub trait ClipboardDb { - fn store_entry(&self, input: impl Read, max_dedupe_search: u64, max_items: u64); - fn deduplicate(&self, buf: &[u8], max: u64); - fn trim_db(&self, max: u64); - fn delete_last(&self); - fn wipe_db(&self); - fn list_entries(&self, out: impl Write, preview_width: u32); - fn decode_entry(&self, in_: impl Read, out: impl Write, input: Option); - fn delete_query(&self, query: &str); - fn delete_entries(&self, in_: impl Read); + fn store_entry( + &self, + input: impl Read, + max_dedupe_search: u64, + max_items: u64, + ) -> Result; + fn deduplicate(&self, buf: &[u8], max: u64) -> Result; + fn trim_db(&self, max: u64) -> Result<(), StashError>; + fn delete_last(&self) -> Result<(), StashError>; + fn wipe_db(&self) -> Result<(), StashError>; + fn list_entries(&self, out: impl Write, preview_width: u32) -> Result; + fn decode_entry( + &self, + in_: impl Read, + out: impl Write, + input: Option, + ) -> Result<(), StashError>; + fn delete_query(&self, query: &str) -> Result; + fn delete_entries(&self, in_: impl Read) -> Result; fn next_sequence(&self) -> u64; } @@ -39,20 +94,23 @@ pub struct SledClipboardDb { } impl ClipboardDb for SledClipboardDb { - fn store_entry(&self, mut input: impl Read, max_dedupe_search: u64, max_items: u64) { + fn store_entry( + &self, + mut input: impl Read, + max_dedupe_search: u64, + max_items: u64, + ) -> Result { let mut buf = Vec::new(); if input.read_to_end(&mut buf).is_err() || buf.is_empty() || buf.len() > 5 * 1_000_000 { - warn!("Input is empty or too large, skipping store."); - return; + return Err(StashError::EmptyOrTooLarge); } if buf.iter().all(u8::is_ascii_whitespace) { - warn!("Input is all whitespace, skipping store."); - return; + return Err(StashError::AllWhitespace); } let mime = detect_mime(&buf); - self.deduplicate(&buf, max_dedupe_search); + self.deduplicate(&buf, max_dedupe_search)?; let entry = Entry { contents: buf.clone(), @@ -60,210 +118,160 @@ impl ClipboardDb for SledClipboardDb { }; let id = self.next_sequence(); - let enc = match to_vec(&entry) { - Ok(enc) => enc, - Err(e) => { - error!("Failed to serialize entry: {e}"); - return; - } - }; + let enc = to_vec(&entry).map_err(|e| StashError::Serialize(e.to_string()))?; - match self.db.insert(u64_to_ivec(id), enc) { - Ok(_) => info!("Stored entry with id {id}"), - Err(e) => error!("Failed to store entry: {e}"), - } - self.trim_db(max_items); + self.db + .insert(u64_to_ivec(id), enc) + .map_err(|e| StashError::Store(e.to_string()))?; + self.trim_db(max_items)?; + Ok(id) } - fn deduplicate(&self, buf: &[u8], max: u64) { + fn deduplicate(&self, buf: &[u8], max: u64) -> Result { let mut count = 0; let mut deduped = 0; for item in self.db.iter().rev().take(max as usize) { let (k, v) = match item { Ok((k, v)) => (k, v), - Err(e) => { - error!("Error reading entry during deduplication: {e}"); - continue; - } + Err(e) => return Err(StashError::DeduplicationRead(e.to_string())), }; let entry: Entry = match from_read(v.as_ref()) { Ok(e) => e, - Err(e) => { - error!("Error decoding entry during deduplication: {e}"); - continue; - } + Err(e) => return Err(StashError::DeduplicationDecode(e.to_string())), }; if entry.contents == buf { - match self.db.remove(k) { - Ok(_) => { + self.db + .remove(k) + .map(|_| { deduped += 1; - info!("Deduplicated an entry"); - } - Err(e) => error!("Failed to remove entry during deduplication: {e}"), - } + }) + .map_err(|e| StashError::DeduplicationRemove(e.to_string()))?; } count += 1; if count >= max { break; } } - if deduped > 0 { - info!("Deduplicated {deduped} entries"); - } + Ok(deduped) } - fn trim_db(&self, max: u64) { + 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) => { - error!("Failed to read key during trim: {e}"); - None - } + Err(_e) => None, }) .collect(); - let initial_len = keys.len(); if keys.len() as u64 > max { for k in keys.drain((max as usize)..) { - match self.db.remove(k) { - Ok(_) => info!("Trimmed entry from database"), - Err(e) => error!("Failed to trim entry: {e}"), - } + self.db + .remove(k) + .map_err(|e| StashError::Trim(e.to_string()))?; } - info!( - "Trimmed {} entries from database", - initial_len - max as usize - ); } + Ok(()) } - fn delete_last(&self) { + fn delete_last(&self) -> Result<(), StashError> { if let Some((k, _)) = self.db.iter().next_back().and_then(Result::ok) { - match self.db.remove(k) { - Ok(_) => info!("Deleted last entry"), - Err(e) => error!("Failed to delete last entry: {e}"), - } + self.db + .remove(k) + .map(|_| ()) + .map_err(|e| StashError::DeleteLast(e.to_string())) } else { - warn!("No entries to delete"); + Err(StashError::NoEntriesToDelete) } } - fn wipe_db(&self) { - match self.db.clear() { - Ok(()) => info!("Wiped database"), - Err(e) => error!("Failed to wipe database: {e}"), - } + fn wipe_db(&self) -> Result<(), StashError> { + self.db.clear().map_err(|e| StashError::Wipe(e.to_string())) } - fn list_entries(&self, mut out: impl Write, preview_width: u32) { + fn list_entries(&self, mut out: impl Write, preview_width: u32) -> Result { 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) => { - error!("Failed to decode entry during list: {e}"); - continue; - } + Err(e) => return Err(StashError::ListDecode(e.to_string())), }; let preview = preview_entry(&entry.contents, entry.mime.as_deref(), preview_width); if writeln!(out, "{id}\t{preview}").is_ok() { listed += 1; } } - info!("Listed {listed} entries"); + Ok(listed) } - fn decode_entry(&self, mut in_: impl Read, mut out: impl Write, input: Option) { + fn decode_entry( + &self, + mut in_: impl Read, + mut out: impl Write, + input: Option, + ) -> Result<(), StashError> { let s = if let Some(input) = input { input } else { let mut buf = String::new(); - if let Err(e) = in_.read_to_string(&mut buf) { - error!("Failed to read input for decode: {e}"); - return; - } + in_.read_to_string(&mut buf) + .map_err(|e| StashError::DecodeRead(e.to_string()))?; buf }; - let id = match extract_id(&s) { - Ok(id) => id, - Err(e) => { - error!("Failed to extract id for decode: {e}"); - return; - } - }; - let v = match self.db.get(u64_to_ivec(id)) { - Ok(Some(v)) => v, - Ok(None) => { - warn!("No entry found for id {id}"); - return; - } - Err(e) => { - error!("Failed to get entry for decode: {e}"); - return; - } - }; - let entry: Entry = match from_read(v.as_ref()) { - Ok(e) => e, - Err(e) => { - error!("Failed to decode entry: {e}"); - return; - } - }; - if let Err(e) = out.write_all(&entry.contents) { - error!("Failed to write decoded entry: {e}"); - } else { - info!("Decoded entry with id {id}"); - } + 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_else(|| StashError::DecodeNoEntry(id))?; + let entry: Entry = + from_read(v.as_ref()).map_err(|e| StashError::DecodeDecode(e.to_string()))?; + + out.write_all(&entry.contents) + .map_err(|e| StashError::DecodeWrite(e.to_string()))?; + info!("Decoded entry with id {id}"); + Ok(()) } - fn delete_query(&self, query: &str) { + fn delete_query(&self, query: &str) -> Result { 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(e) => { - error!("Failed to decode entry during query delete: {e}"); - continue; - } + Err(_) => continue, }; if entry .contents .windows(query.len()) .any(|w| w == query.as_bytes()) { - match self.db.remove(k) { - Ok(_) => { + self.db + .remove(k) + .map(|_| { deleted += 1; - info!("Deleted entry matching query"); - } - Err(e) => error!("Failed to delete entry during query delete: {e}"), - } + }) + .map_err(|e| StashError::QueryDelete(e.to_string()))?; } } - info!("Deleted {deleted} entries matching query '{query}'"); + Ok(deleted) } - fn delete_entries(&self, in_: impl Read) { + fn delete_entries(&self, in_: impl Read) -> Result { let reader = BufReader::new(in_); let mut deleted = 0; for line in reader.lines().map_while(Result::ok) { if let Ok(id) = extract_id(&line) { - match self.db.remove(u64_to_ivec(id)) { - Ok(_) => { + self.db + .remove(u64_to_ivec(id)) + .map(|_| { deleted += 1; - info!("Deleted entry with id {id}"); - } - Err(e) => error!("Failed to delete entry with id {id}: {e}"), - } - } else { - warn!("Failed to extract id from line: {line}"); + }) + .map_err(|e| StashError::DeleteEntry(id, e.to_string()))?; } } - info!("Deleted {deleted} entries by id from stdin"); + Ok(deleted) } fn next_sequence(&self) -> u64 { diff --git a/src/main.rs b/src/main.rs index b79f9cd..97e4d7d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -98,43 +98,61 @@ fn main() { match cli.command { Some(Command::Store) => { let state = env::var("STASH_CLIPBOARD_STATE").ok(); - db.store(io::stdin(), cli.max_dedupe_search, cli.max_items, state); + if let Err(e) = db.store(io::stdin(), cli.max_dedupe_search, cli.max_items, state) { + log::error!("Failed to store entry: {e}"); + } } Some(Command::List) => { - db.list(io::stdout(), cli.preview_width); + if let Err(e) = db.list(io::stdout(), cli.preview_width) { + log::error!("Failed to list entries: {e}"); + } } Some(Command::Decode { input }) => { - db.decode(io::stdin(), io::stdout(), input); + if let Err(e) = db.decode(io::stdin(), io::stdout(), input) { + log::error!("Failed to decode entry: {e}"); + } } Some(Command::Delete { arg, r#type }) => match (arg, r#type.as_deref()) { (Some(s), Some("id")) => { if let Ok(id) = s.parse::() { use std::io::Cursor; - db.delete(Cursor::new(format!("{id}\n"))); + if let Err(e) = db.delete(Cursor::new(format!("{id}\n"))) { + log::error!("Failed to delete entry by id: {e}"); + } } else { log::error!("Argument is not a valid id"); } } (Some(s), Some("query")) => { - db.query_delete(&s); + if let Err(e) = db.query_delete(&s) { + log::error!("Failed to delete entry by query: {e}"); + } } (Some(s), None) => { if let Ok(id) = s.parse::() { use std::io::Cursor; - db.delete(Cursor::new(format!("{id}\n"))); + if let Err(e) = db.delete(Cursor::new(format!("{id}\n"))) { + log::error!("Failed to delete entry by id: {e}"); + } } else { - db.query_delete(&s); + if let Err(e) = db.query_delete(&s) { + log::error!("Failed to delete entry by query: {e}"); + } } } (None, _) => { - db.delete(io::stdin()); + if let Err(e) = db.delete(io::stdin()) { + log::error!("Failed to delete entry from stdin: {e}"); + } } (_, Some(_)) => { log::error!("Unknown type for --type. Use \"id\" or \"query\"."); } }, Some(Command::Wipe) => { - db.wipe(); + if let Err(e) = db.wipe() { + log::error!("Failed to wipe database: {e}"); + } } Some(Command::Import { r#type }) => {