treewide: improve logging; get rid of unwrap()s

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I6a6a696442ff25c3f65bb2d5f68e0d78a569fd76
This commit is contained in:
raf 2025-08-12 14:40:53 +03:00
commit 254c288111
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
11 changed files with 324 additions and 69 deletions

102
Cargo.lock generated
View file

@ -8,6 +8,15 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "aligned-vec" name = "aligned-vec"
version = "0.6.4" version = "0.6.4"
@ -325,6 +334,29 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "env_filter"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
dependencies = [
"log",
"regex",
]
[[package]]
name = "env_logger"
version = "0.11.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"jiff",
"log",
]
[[package]] [[package]]
name = "equator" name = "equator"
version = "0.4.2" version = "0.4.2"
@ -543,6 +575,30 @@ dependencies = [
"either", "either",
] ]
[[package]]
name = "jiff"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49"
dependencies = [
"jiff-static",
"log",
"portable-atomic",
"portable-atomic-util",
"serde",
]
[[package]]
name = "jiff-static"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "jobserver" name = "jobserver"
version = "0.1.33" version = "0.1.33"
@ -788,6 +844,21 @@ dependencies = [
"miniz_oxide", "miniz_oxide",
] ]
[[package]]
name = "portable-atomic"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
[[package]]
name = "portable-atomic-util"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
dependencies = [
"portable-atomic",
]
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.21" version = "0.2.21"
@ -975,6 +1046,35 @@ dependencies = [
"thiserror 2.0.14", "thiserror 2.0.14",
] ]
[[package]]
name = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]] [[package]]
name = "rgb" name = "rgb"
version = "0.8.52" version = "0.8.52"
@ -1093,7 +1193,9 @@ version = "0.1.0"
dependencies = [ dependencies = [
"clap", "clap",
"dirs", "dirs",
"env_logger",
"image", "image",
"log",
"rmp-serde", "rmp-serde",
"serde", "serde",
"sled", "sled",

View file

@ -2,6 +2,7 @@
name = "stash" name = "stash"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
author = "NotAShelf <raf@notashelf.dev>"
[dependencies] [dependencies]
clap = { version = "4.5.44", features = ["derive"] } clap = { version = "4.5.44", features = ["derive"] }
@ -10,3 +11,5 @@ dirs = "6.0.0"
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
rmp-serde = "1.3.0" rmp-serde = "1.3.0"
image = "0.25.6" image = "0.25.6"
log = "0.4.27"
env_logger = "0.11.8"

View file

@ -1,4 +1,5 @@
use crate::db::{ClipboardDb, SledClipboardDb}; use crate::db::{ClipboardDb, SledClipboardDb};
use std::io::{Read, Write}; use std::io::{Read, Write};
pub trait DecodeCommand { pub trait DecodeCommand {
@ -8,5 +9,6 @@ pub trait DecodeCommand {
impl DecodeCommand for SledClipboardDb { impl DecodeCommand for SledClipboardDb {
fn decode(&self, in_: impl Read, out: impl Write, input: Option<String>) { fn decode(&self, in_: impl Read, out: impl Write, input: Option<String>) {
self.decode_entry(in_, out, input); self.decode_entry(in_, out, input);
log::info!("Entry decoded");
} }
} }

View file

@ -1,4 +1,5 @@
use crate::db::{ClipboardDb, SledClipboardDb}; use crate::db::{ClipboardDb, SledClipboardDb};
use std::io::Read; use std::io::Read;
pub trait DeleteCommand { pub trait DeleteCommand {
@ -8,5 +9,6 @@ pub trait DeleteCommand {
impl DeleteCommand for SledClipboardDb { impl DeleteCommand for SledClipboardDb {
fn delete(&self, input: impl Read) { fn delete(&self, input: impl Read) {
self.delete_entries(input); self.delete_entries(input);
log::info!("Entries deleted");
} }
} }

View file

@ -8,5 +8,6 @@ pub trait ListCommand {
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) {
self.list_entries(out, preview_width); self.list_entries(out, preview_width);
log::info!("Entries listed");
} }
} }

View file

@ -7,5 +7,6 @@ pub trait QueryCommand {
impl QueryCommand for SledClipboardDb { impl QueryCommand for SledClipboardDb {
fn query_delete(&self, query: &str) { fn query_delete(&self, query: &str) {
<SledClipboardDb as ClipboardDb>::delete_query(self, query); <SledClipboardDb as ClipboardDb>::delete_query(self, query);
log::info!("Entries matching query '{}' deleted", query);
} }
} }

View file

@ -1,4 +1,5 @@
use crate::db::{ClipboardDb, SledClipboardDb}; use crate::db::{ClipboardDb, SledClipboardDb};
use std::io::Read; use std::io::Read;
pub trait StoreCommand { pub trait StoreCommand {
@ -19,13 +20,12 @@ impl StoreCommand for SledClipboardDb {
max_items: u64, max_items: u64,
state: Option<String>, state: Option<String>,
) { ) {
match state.as_deref() { if let Some("sensitive" | "clear") = state.as_deref() {
Some("sensitive") | Some("clear") => {
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");
} }
} }
} }

View file

@ -7,5 +7,6 @@ pub trait WipeCommand {
impl WipeCommand for SledClipboardDb { impl WipeCommand for SledClipboardDb {
fn wipe(&self) { fn wipe(&self) {
self.wipe_db(); self.wipe_db();
log::info!("Database wiped");
} }
} }

View file

@ -3,6 +3,7 @@ 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 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};
@ -41,9 +42,11 @@ 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) {
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; return;
} }
if buf.iter().all(|b| b.is_ascii_whitespace()) { if buf.iter().all(u8::is_ascii_whitespace) {
warn!("Input is all whitespace, skipping store.");
return; return;
} }
@ -57,53 +60,121 @@ impl ClipboardDb for SledClipboardDb {
}; };
let id = self.next_sequence(); let id = self.next_sequence();
let enc = to_vec(&entry).unwrap(); let enc = match to_vec(&entry) {
Ok(enc) => enc,
Err(e) => {
error!("Failed to serialize entry: {e}");
return;
}
};
self.db.insert(u64_to_ivec(id), enc).unwrap(); 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.trim_db(max_items);
} }
fn deduplicate(&self, buf: &[u8], max: u64) { fn deduplicate(&self, buf: &[u8], max: u64) {
let mut count = 0; let mut count = 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) = item.unwrap(); let (k, v) = match item {
let entry: Entry = from_read(v.as_ref()).unwrap(); Ok((k, v)) => (k, v),
Err(e) => {
error!("Error reading entry during deduplication: {e}");
continue;
}
};
let entry: Entry = match from_read(v.as_ref()) {
Ok(e) => e,
Err(e) => {
error!("Error decoding entry during deduplication: {e}");
continue;
}
};
if entry.contents == buf { if entry.contents == buf {
self.db.remove(k).unwrap(); match self.db.remove(k) {
Ok(_) => {
deduped += 1;
info!("Deduplicated an entry");
}
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 {
info!("Deduplicated {deduped} entries");
}
} }
fn trim_db(&self, max: u64) { fn trim_db(&self, max: u64) {
let mut keys: Vec<_> = self.db.iter().rev().map(|kv| kv.unwrap().0).collect(); 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
}
})
.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)..) {
self.db.remove(k).unwrap(); match self.db.remove(k) {
Ok(_) => info!("Trimmed entry from database"),
Err(e) => error!("Failed to trim entry: {e}"),
} }
} }
info!(
"Trimmed {} entries from database",
initial_len - max as usize
);
}
} }
fn delete_last(&self) { fn delete_last(&self) {
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) {
self.db.remove(k).unwrap(); match self.db.remove(k) {
Ok(_) => info!("Deleted last entry"),
Err(e) => error!("Failed to delete last entry: {e}"),
}
} else {
warn!("No entries to delete");
} }
} }
fn wipe_db(&self) { fn wipe_db(&self) {
self.db.clear().unwrap(); match self.db.clear() {
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) {
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 = from_read(v.as_ref()).unwrap(); let entry: Entry = match from_read(v.as_ref()) {
let preview = preview_entry(&entry.contents, entry.mime.as_deref(), preview_width); Ok(e) => e,
writeln!(out, "{id}\t{preview}").unwrap(); Err(e) => {
error!("Failed to decode entry during list: {e}");
continue;
} }
};
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");
} }
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>) {
@ -111,36 +182,88 @@ impl ClipboardDb for SledClipboardDb {
input input
} else { } else {
let mut buf = String::new(); let mut buf = String::new();
in_.read_to_string(&mut buf).unwrap(); if let Err(e) = in_.read_to_string(&mut buf) {
error!("Failed to read input for decode: {e}");
return;
}
buf buf
}; };
let id = match extract_id(&s) {
let id = extract_id(&s).unwrap(); Ok(id) => id,
let v = self.db.get(u64_to_ivec(id)).unwrap().unwrap(); Err(e) => {
let entry: Entry = from_read(v.as_ref()).unwrap(); error!("Failed to extract id for decode: {e}");
out.write_all(&entry.contents).unwrap(); 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}");
}
} }
fn delete_query(&self, query: &str) { fn delete_query(&self, query: &str) {
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 = from_read(v.as_ref()).unwrap(); let entry: Entry = match from_read(v.as_ref()) {
Ok(e) => e,
Err(e) => {
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())
{ {
self.db.remove(k).unwrap(); match self.db.remove(k) {
Ok(_) => {
deleted += 1;
info!("Deleted entry matching query");
}
Err(e) => error!("Failed to delete entry during query delete: {e}"),
} }
} }
} }
info!("Deleted {deleted} entries matching query '{query}'");
}
fn delete_entries(&self, in_: impl Read) { fn delete_entries(&self, in_: impl Read) {
let reader = BufReader::new(in_); let reader = BufReader::new(in_);
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) {
self.db.remove(u64_to_ivec(id)).unwrap(); match self.db.remove(u64_to_ivec(id)) {
Ok(_) => {
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}");
} }
} }
info!("Deleted {deleted} entries by id from stdin");
} }
fn next_sequence(&self) -> u64 { fn next_sequence(&self) -> u64 {
@ -148,7 +271,7 @@ impl ClipboardDb for SledClipboardDb {
.db .db
.iter() .iter()
.next_back() .next_back()
.and_then(|r| r.ok()) .and_then(std::result::Result::ok)
.map(|(k, _)| ivec_to_u64(&k)); .map(|(k, _)| ivec_to_u64(&k));
last.unwrap_or(0) + 1 last.unwrap_or(0) + 1
} }
@ -165,7 +288,10 @@ pub fn u64_to_ivec(v: u64) -> IVec {
} }
pub fn ivec_to_u64(v: &IVec) -> u64 { pub fn ivec_to_u64(v: &IVec) -> u64 {
let arr: [u8; 8] = v.as_ref().try_into().unwrap(); 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) u64::from_be_bytes(arr)
} }

View file

@ -1,4 +1,5 @@
use crate::db::{Entry, SledClipboardDb, detect_mime, u64_to_ivec}; use crate::db::{Entry, SledClipboardDb, detect_mime, u64_to_ivec};
use log::{error, info};
use std::io::{self, BufRead}; use std::io::{self, BufRead};
pub trait ImportCommand { pub trait ImportCommand {
@ -17,12 +18,27 @@ impl ImportCommand for SledClipboardDb {
contents: val.as_bytes().to_vec(), contents: val.as_bytes().to_vec(),
mime: detect_mime(val.as_bytes()), mime: detect_mime(val.as_bytes()),
}; };
let enc = rmp_serde::encode::to_vec(&entry).unwrap(); let enc = match rmp_serde::encode::to_vec(&entry) {
self.db.insert(u64_to_ivec(id), enc).unwrap(); Ok(enc) => enc,
Err(e) => {
error!("Failed to encode entry for id {id}: {e}");
continue;
}
};
match self.db.insert(u64_to_ivec(id), enc) {
Ok(_) => {
imported += 1; imported += 1;
info!("Imported entry with id {id}");
}
Err(e) => error!("Failed to insert entry with id {id}: {e}"),
}
} else {
error!("Failed to parse id from line: {id_str}");
}
} else {
error!("Malformed TSV line: {line:?}");
} }
} }
} info!("Imported {imported} records from TSV into sled database.");
eprintln!("Imported {imported} records from TSV into sled database.");
} }
} }

View file

@ -76,6 +76,7 @@ enum Command {
} }
fn main() { fn main() {
env_logger::init();
let cli = Cli::parse(); let cli = Cli::parse();
let db_path = cli.db_path.unwrap_or_else(|| { let db_path = cli.db_path.unwrap_or_else(|| {
dirs::cache_dir() dirs::cache_dir()
@ -85,10 +86,9 @@ fn main() {
}); });
let sled_db = sled::open(&db_path).unwrap_or_else(|e| { let sled_db = sled::open(&db_path).unwrap_or_else(|e| {
eprintln!("Failed to open database: {e}"); log::error!("Failed to open database: {e}");
process::exit(1); process::exit(1);
}); });
let db = db::SledClipboardDb { db: sled_db }; let db = db::SledClipboardDb { db: sled_db };
if cli.import_tsv { if cli.import_tsv {
@ -98,32 +98,32 @@ fn main() {
match cli.command { match cli.command {
Some(Command::Store) => { Some(Command::Store) => {
let state = env::var("STASH_CLIPBOARD_STATE").ok(); log::info!("Executing: Store");
let state = env::var("CLIPBOARD_STATE").ok();
db.store(io::stdin(), cli.max_dedupe_search, cli.max_items, state); db.store(io::stdin(), cli.max_dedupe_search, cli.max_items, state);
} }
Some(Command::List) => { Some(Command::List) => {
log::info!("Executing: List");
db.list(io::stdout(), cli.preview_width); db.list(io::stdout(), cli.preview_width);
} }
Some(Command::Decode { input }) => { Some(Command::Decode { input }) => {
log::info!("Executing: Decode");
db.decode(io::stdin(), io::stdout(), input); db.decode(io::stdin(), io::stdout(), input);
} }
Some(Command::Delete { arg, r#type }) => {
Some(Command::Delete { arg, r#type }) => match (arg, r#type.as_deref()) { log::info!("Executing: Delete");
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"))); db.delete(Cursor::new(format!("{id}\n")));
} else { } else {
eprintln!("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); db.query_delete(&s);
} }
(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;
@ -132,19 +132,20 @@ fn main() {
db.query_delete(&s); db.query_delete(&s);
} }
} }
(None, _) => { (None, _) => {
db.delete(io::stdin()); db.delete(io::stdin());
} }
(_, Some(_)) => { (_, Some(_)) => {
eprintln!("Unknown type for --type. Use \"id\" or \"query\"."); log::error!("Unknown type for --type. Use \"id\" or \"query\".");
}
}
} }
},
Some(Command::Wipe) => { Some(Command::Wipe) => {
log::info!("Executing: Wipe");
db.wipe(); db.wipe();
} }
Some(Command::Import { r#type }) => { Some(Command::Import { r#type }) => {
log::info!("Executing: Import");
// Default format is TSV (Cliphist compatible) // Default format is TSV (Cliphist compatible)
let format = r#type.as_deref().unwrap_or("tsv"); let format = r#type.as_deref().unwrap_or("tsv");
match format { match format {
@ -152,12 +153,12 @@ fn main() {
db.import_tsv(io::stdin()); db.import_tsv(io::stdin());
} }
_ => { _ => {
eprintln!("Unsupported import format: {format}"); log::error!("Unsupported import format: {format}");
} }
} }
} }
_ => { _ => {
eprintln!("No subcommand provided"); log::warn!("No subcommand provided");
} }
} }
} }