treewide: improve logging; custom error types with thiserror

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I6a6a696464e4123d15cfaedf4727776e55948369
This commit is contained in:
raf 2025-08-12 16:08:28 +03:00
commit 6e21021306
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
10 changed files with 221 additions and 166 deletions

1
Cargo.lock generated
View file

@ -1210,6 +1210,7 @@ dependencies = [
"rmp-serde", "rmp-serde",
"serde", "serde",
"sled", "sled",
"thiserror 2.0.14",
] ]
[[package]] [[package]]

View file

@ -18,3 +18,4 @@ image = "0.25.6"
log = "0.4.27" log = "0.4.27"
env_logger = "0.11.8" env_logger = "0.11.8"
clap-verbosity-flag = "3.0.3" clap-verbosity-flag = "3.0.3"
thiserror = "2.0.14"

View file

@ -2,13 +2,26 @@ use crate::db::{ClipboardDb, SledClipboardDb};
use std::io::{Read, Write}; use std::io::{Read, Write};
use crate::db::StashError;
pub trait DecodeCommand { pub trait DecodeCommand {
fn decode(&self, in_: impl Read, out: impl Write, input: Option<String>); fn decode(
&self,
in_: impl Read,
out: impl Write,
input: Option<String>,
) -> Result<(), StashError>;
} }
impl DecodeCommand for SledClipboardDb { impl DecodeCommand for SledClipboardDb {
fn decode(&self, in_: impl Read, out: impl Write, input: Option<String>) { fn decode(
self.decode_entry(in_, out, input); &self,
in_: impl Read,
out: impl Write,
input: Option<String>,
) -> Result<(), StashError> {
self.decode_entry(in_, out, input)?;
log::info!("Entry decoded"); log::info!("Entry decoded");
Ok(())
} }
} }

View file

@ -1,14 +1,22 @@
use crate::db::{ClipboardDb, SledClipboardDb}; use crate::db::{ClipboardDb, SledClipboardDb, StashError};
use std::io::Read; use std::io::Read;
pub trait DeleteCommand { pub trait DeleteCommand {
fn delete(&self, input: impl Read); fn delete(&self, input: impl Read) -> Result<usize, StashError>;
} }
impl DeleteCommand for SledClipboardDb { impl DeleteCommand for SledClipboardDb {
fn delete(&self, input: impl Read) { fn delete(&self, input: impl Read) -> Result<usize, StashError> {
self.delete_entries(input); match self.delete_entries(input) {
log::info!("Entries deleted"); Ok(deleted) => {
log::info!("Deleted {} entries", deleted);
Ok(deleted)
}
Err(e) => {
log::error!("Failed to delete entries: {}", e);
Err(e)
}
}
} }
} }

View file

@ -2,12 +2,13 @@ use crate::db::{ClipboardDb, SledClipboardDb};
use std::io::Write; use std::io::Write;
pub trait ListCommand { 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 { impl ListCommand for SledClipboardDb {
fn list(&self, out: impl Write, preview_width: u32) { fn list(&self, out: impl Write, preview_width: u32) -> Result<(), crate::db::StashError> {
self.list_entries(out, preview_width); self.list_entries(out, preview_width)?;
log::info!("Entries listed"); log::info!("Listed clipboard entries");
Ok(())
} }
} }

View file

@ -1,12 +1,13 @@
use crate::db::{ClipboardDb, SledClipboardDb}; use crate::db::{ClipboardDb, SledClipboardDb};
use crate::db::StashError;
pub trait QueryCommand { pub trait QueryCommand {
fn query_delete(&self, query: &str); fn query_delete(&self, query: &str) -> Result<usize, StashError>;
} }
impl QueryCommand for SledClipboardDb { impl QueryCommand for SledClipboardDb {
fn query_delete(&self, query: &str) { fn query_delete(&self, query: &str) -> Result<usize, StashError> {
<SledClipboardDb as ClipboardDb>::delete_query(self, query); <SledClipboardDb as ClipboardDb>::delete_query(self, query)
log::info!("Entries matching query '{}' deleted", query);
} }
} }

View file

@ -9,7 +9,7 @@ pub trait StoreCommand {
max_dedupe_search: u64, max_dedupe_search: u64,
max_items: u64, max_items: u64,
state: Option<String>, state: Option<String>,
); ) -> Result<(), crate::db::StashError>;
} }
impl StoreCommand for SledClipboardDb { impl StoreCommand for SledClipboardDb {
@ -19,13 +19,14 @@ impl StoreCommand for SledClipboardDb {
max_dedupe_search: u64, max_dedupe_search: u64,
max_items: u64, max_items: u64,
state: Option<String>, state: Option<String>,
) { ) -> Result<(), crate::db::StashError> {
if let Some("sensitive" | "clear") = state.as_deref() { if let Some("sensitive" | "clear") = state.as_deref() {
self.delete_last(); self.delete_last()?;
log::info!("Entry deleted"); log::info!("Entry deleted");
} else { } else {
self.store_entry(input, max_dedupe_search, max_items); self.store_entry(input, max_dedupe_search, max_items)?;
log::info!("Entry stored"); log::info!("Entry stored");
} }
Ok(())
} }
} }

View file

@ -1,12 +1,15 @@
use crate::db::{ClipboardDb, SledClipboardDb}; use crate::db::{ClipboardDb, SledClipboardDb};
use crate::db::StashError;
pub trait WipeCommand { pub trait WipeCommand {
fn wipe(&self); fn wipe(&self) -> Result<(), StashError>;
} }
impl WipeCommand for SledClipboardDb { impl WipeCommand for SledClipboardDb {
fn wipe(&self) { fn wipe(&self) -> Result<(), StashError> {
self.wipe_db(); self.wipe_db()?;
log::info!("Database wiped"); log::info!("Database wiped");
Ok(())
} }
} }

View file

@ -3,21 +3,76 @@ use std::io::{BufRead, BufReader, Read, Write};
use std::str; use std::str;
use image::{GenericImageView, ImageFormat}; use image::{GenericImageView, ImageFormat};
use log::{error, info, warn}; use log::{error, info};
use rmp_serde::{decode::from_read, encode::to_vec}; use rmp_serde::{decode::from_read, encode::to_vec};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sled::{Db, IVec}; 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 { pub trait ClipboardDb {
fn store_entry(&self, input: impl Read, max_dedupe_search: u64, max_items: u64); fn store_entry(
fn deduplicate(&self, buf: &[u8], max: u64); &self,
fn trim_db(&self, max: u64); input: impl Read,
fn delete_last(&self); max_dedupe_search: u64,
fn wipe_db(&self); max_items: u64,
fn list_entries(&self, out: impl Write, preview_width: u32); ) -> Result<u64, StashError>;
fn decode_entry(&self, in_: impl Read, out: impl Write, input: Option<String>); fn deduplicate(&self, buf: &[u8], max: u64) -> Result<usize, StashError>;
fn delete_query(&self, query: &str); fn trim_db(&self, max: u64) -> Result<(), StashError>;
fn delete_entries(&self, in_: impl Read); fn delete_last(&self) -> Result<(), StashError>;
fn wipe_db(&self) -> Result<(), StashError>;
fn list_entries(&self, out: impl Write, preview_width: u32) -> Result<usize, StashError>;
fn decode_entry(
&self,
in_: impl Read,
out: impl Write,
input: Option<String>,
) -> Result<(), StashError>;
fn delete_query(&self, query: &str) -> Result<usize, StashError>;
fn delete_entries(&self, in_: impl Read) -> Result<usize, StashError>;
fn next_sequence(&self) -> u64; fn next_sequence(&self) -> u64;
} }
@ -39,20 +94,23 @@ pub struct SledClipboardDb {
} }
impl ClipboardDb for 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<u64, StashError> {
let mut buf = Vec::new(); let mut buf = Vec::new();
if input.read_to_end(&mut buf).is_err() || buf.is_empty() || buf.len() > 5 * 1_000_000 { 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 Err(StashError::EmptyOrTooLarge);
return;
} }
if buf.iter().all(u8::is_ascii_whitespace) { if buf.iter().all(u8::is_ascii_whitespace) {
warn!("Input is all whitespace, skipping store."); return Err(StashError::AllWhitespace);
return;
} }
let mime = detect_mime(&buf); let mime = detect_mime(&buf);
self.deduplicate(&buf, max_dedupe_search); self.deduplicate(&buf, max_dedupe_search)?;
let entry = Entry { let entry = Entry {
contents: buf.clone(), contents: buf.clone(),
@ -60,210 +118,160 @@ impl ClipboardDb for SledClipboardDb {
}; };
let id = self.next_sequence(); let id = self.next_sequence();
let enc = match to_vec(&entry) { let enc = to_vec(&entry).map_err(|e| StashError::Serialize(e.to_string()))?;
Ok(enc) => enc,
Err(e) => {
error!("Failed to serialize entry: {e}");
return;
}
};
match self.db.insert(u64_to_ivec(id), enc) { self.db
Ok(_) => info!("Stored entry with id {id}"), .insert(u64_to_ivec(id), enc)
Err(e) => error!("Failed to store entry: {e}"), .map_err(|e| StashError::Store(e.to_string()))?;
} self.trim_db(max_items)?;
self.trim_db(max_items); Ok(id)
} }
fn deduplicate(&self, buf: &[u8], max: u64) { fn deduplicate(&self, buf: &[u8], max: u64) -> Result<usize, StashError> {
let mut count = 0; let mut count = 0;
let mut deduped = 0; let mut deduped = 0;
for item in self.db.iter().rev().take(max as usize) { for item in self.db.iter().rev().take(max as usize) {
let (k, v) = match item { let (k, v) = match item {
Ok((k, v)) => (k, v), Ok((k, v)) => (k, v),
Err(e) => { Err(e) => return Err(StashError::DeduplicationRead(e.to_string())),
error!("Error reading entry during deduplication: {e}");
continue;
}
}; };
let entry: Entry = match from_read(v.as_ref()) { let entry: Entry = match from_read(v.as_ref()) {
Ok(e) => e, Ok(e) => e,
Err(e) => { Err(e) => return Err(StashError::DeduplicationDecode(e.to_string())),
error!("Error decoding entry during deduplication: {e}");
continue;
}
}; };
if entry.contents == buf { if entry.contents == buf {
match self.db.remove(k) { self.db
Ok(_) => { .remove(k)
.map(|_| {
deduped += 1; deduped += 1;
info!("Deduplicated an entry"); })
} .map_err(|e| StashError::DeduplicationRemove(e.to_string()))?;
Err(e) => error!("Failed to remove entry during deduplication: {e}"),
}
} }
count += 1; count += 1;
if count >= max { if count >= max {
break; break;
} }
} }
if deduped > 0 { Ok(deduped)
info!("Deduplicated {deduped} entries");
}
} }
fn trim_db(&self, max: u64) { fn trim_db(&self, max: u64) -> Result<(), StashError> {
let mut keys: Vec<_> = self let mut keys: Vec<_> = self
.db .db
.iter() .iter()
.rev() .rev()
.filter_map(|kv| match kv { .filter_map(|kv| match kv {
Ok((k, _)) => Some(k), Ok((k, _)) => Some(k),
Err(e) => { Err(_e) => None,
error!("Failed to read key during trim: {e}");
None
}
}) })
.collect(); .collect();
let initial_len = keys.len();
if keys.len() as u64 > max { if keys.len() as u64 > max {
for k in keys.drain((max as usize)..) { for k in keys.drain((max as usize)..) {
match self.db.remove(k) { self.db
Ok(_) => info!("Trimmed entry from database"), .remove(k)
Err(e) => error!("Failed to trim entry: {e}"), .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) { if let Some((k, _)) = self.db.iter().next_back().and_then(Result::ok) {
match self.db.remove(k) { self.db
Ok(_) => info!("Deleted last entry"), .remove(k)
Err(e) => error!("Failed to delete last entry: {e}"), .map(|_| ())
} .map_err(|e| StashError::DeleteLast(e.to_string()))
} else { } else {
warn!("No entries to delete"); Err(StashError::NoEntriesToDelete)
} }
} }
fn wipe_db(&self) { fn wipe_db(&self) -> Result<(), StashError> {
match self.db.clear() { self.db.clear().map_err(|e| StashError::Wipe(e.to_string()))
Ok(()) => info!("Wiped database"),
Err(e) => error!("Failed to wipe database: {e}"),
}
} }
fn list_entries(&self, mut out: impl Write, preview_width: u32) { fn list_entries(&self, mut out: impl Write, preview_width: u32) -> Result<usize, StashError> {
let mut listed = 0; let mut listed = 0;
for (k, v) in self.db.iter().rev().filter_map(Result::ok) { for (k, v) in self.db.iter().rev().filter_map(Result::ok) {
let id = ivec_to_u64(&k); let id = ivec_to_u64(&k);
let entry: Entry = match from_read(v.as_ref()) { let entry: Entry = match from_read(v.as_ref()) {
Ok(e) => e, Ok(e) => e,
Err(e) => { Err(e) => return Err(StashError::ListDecode(e.to_string())),
error!("Failed to decode entry during list: {e}");
continue;
}
}; };
let preview = preview_entry(&entry.contents, entry.mime.as_deref(), preview_width); let preview = preview_entry(&entry.contents, entry.mime.as_deref(), preview_width);
if writeln!(out, "{id}\t{preview}").is_ok() { if writeln!(out, "{id}\t{preview}").is_ok() {
listed += 1; listed += 1;
} }
} }
info!("Listed {listed} entries"); Ok(listed)
} }
fn decode_entry(&self, mut in_: impl Read, mut out: impl Write, input: Option<String>) { fn decode_entry(
&self,
mut in_: impl Read,
mut out: impl Write,
input: Option<String>,
) -> Result<(), StashError> {
let s = if let Some(input) = input { let s = if let Some(input) = input {
input input
} else { } else {
let mut buf = String::new(); let mut buf = String::new();
if let Err(e) = in_.read_to_string(&mut buf) { in_.read_to_string(&mut buf)
error!("Failed to read input for decode: {e}"); .map_err(|e| StashError::DecodeRead(e.to_string()))?;
return;
}
buf buf
}; };
let id = match extract_id(&s) { let id = extract_id(&s).map_err(|e| StashError::DecodeExtractId(e.to_string()))?;
Ok(id) => id, let v = self
Err(e) => { .db
error!("Failed to extract id for decode: {e}"); .get(u64_to_ivec(id))
return; .map_err(|e| StashError::DecodeGet(e.to_string()))?
} .ok_or_else(|| StashError::DecodeNoEntry(id))?;
}; let entry: Entry =
let v = match self.db.get(u64_to_ivec(id)) { from_read(v.as_ref()).map_err(|e| StashError::DecodeDecode(e.to_string()))?;
Ok(Some(v)) => v,
Ok(None) => { out.write_all(&entry.contents)
warn!("No entry found for id {id}"); .map_err(|e| StashError::DecodeWrite(e.to_string()))?;
return; info!("Decoded entry with id {id}");
} Ok(())
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}");
}
} }
fn delete_query(&self, query: &str) { fn delete_query(&self, query: &str) -> Result<usize, StashError> {
let mut deleted = 0; let mut deleted = 0;
for (k, v) in self.db.iter().filter_map(Result::ok) { for (k, v) in self.db.iter().filter_map(Result::ok) {
let entry: Entry = match from_read(v.as_ref()) { let entry: Entry = match from_read(v.as_ref()) {
Ok(e) => e, Ok(e) => e,
Err(e) => { Err(_) => continue,
error!("Failed to decode entry during query delete: {e}");
continue;
}
}; };
if entry if entry
.contents .contents
.windows(query.len()) .windows(query.len())
.any(|w| w == query.as_bytes()) .any(|w| w == query.as_bytes())
{ {
match self.db.remove(k) { self.db
Ok(_) => { .remove(k)
.map(|_| {
deleted += 1; deleted += 1;
info!("Deleted entry matching query"); })
} .map_err(|e| StashError::QueryDelete(e.to_string()))?;
Err(e) => error!("Failed to delete entry during query delete: {e}"),
}
} }
} }
info!("Deleted {deleted} entries matching query '{query}'"); Ok(deleted)
} }
fn delete_entries(&self, in_: impl Read) { fn delete_entries(&self, in_: impl Read) -> Result<usize, StashError> {
let reader = BufReader::new(in_); let reader = BufReader::new(in_);
let mut deleted = 0; let mut deleted = 0;
for line in reader.lines().map_while(Result::ok) { for line in reader.lines().map_while(Result::ok) {
if let Ok(id) = extract_id(&line) { if let Ok(id) = extract_id(&line) {
match self.db.remove(u64_to_ivec(id)) { self.db
Ok(_) => { .remove(u64_to_ivec(id))
.map(|_| {
deleted += 1; deleted += 1;
info!("Deleted entry with id {id}"); })
} .map_err(|e| StashError::DeleteEntry(id, e.to_string()))?;
Err(e) => error!("Failed to delete entry with id {id}: {e}"),
}
} else {
warn!("Failed to extract id from line: {line}");
} }
} }
info!("Deleted {deleted} entries by id from stdin"); Ok(deleted)
} }
fn next_sequence(&self) -> u64 { fn next_sequence(&self) -> u64 {

View file

@ -98,43 +98,61 @@ fn main() {
match cli.command { match cli.command {
Some(Command::Store) => { Some(Command::Store) => {
let state = env::var("STASH_CLIPBOARD_STATE").ok(); 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) => { 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 }) => { 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(Command::Delete { arg, r#type }) => match (arg, r#type.as_deref()) {
(Some(s), Some("id")) => { (Some(s), Some("id")) => {
if let Ok(id) = s.parse::<u64>() { if let Ok(id) = s.parse::<u64>() {
use std::io::Cursor; 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 { } else {
log::error!("Argument is not a valid id"); log::error!("Argument is not a valid id");
} }
} }
(Some(s), Some("query")) => { (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) => { (Some(s), None) => {
if let Ok(id) = s.parse::<u64>() { if let Ok(id) = s.parse::<u64>() {
use std::io::Cursor; 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 { } else {
db.query_delete(&s); if let Err(e) = db.query_delete(&s) {
log::error!("Failed to delete entry by query: {e}");
}
} }
} }
(None, _) => { (None, _) => {
db.delete(io::stdin()); if let Err(e) = db.delete(io::stdin()) {
log::error!("Failed to delete entry from stdin: {e}");
}
} }
(_, Some(_)) => { (_, Some(_)) => {
log::error!("Unknown type for --type. Use \"id\" or \"query\"."); log::error!("Unknown type for --type. Use \"id\" or \"query\".");
} }
}, },
Some(Command::Wipe) => { Some(Command::Wipe) => {
db.wipe(); if let Err(e) = db.wipe() {
log::error!("Failed to wipe database: {e}");
}
} }
Some(Command::Import { r#type }) => { Some(Command::Import { r#type }) => {