use std::{io::{Read, stdin}, path::Path};
use content_inspector::ContentType;
use image::GenericImageView;
use miette::{Context, IntoDiagnostic, Result, bail};
use tracing::instrument;
use crate::{cli::StoreArgs, database::{data::ClipboardEntry, init_db, queries::{delete_all_entries, delete_entries_older_than, trim_entries, upsert_entry}}, utils::{decode_image, get_mimetype, now}};
#[instrument]
pub fn execute(path_db: &Path, args: StoreArgs) -> Result<()> {
execute_with_source(path_db, args, stdin())
}
#[doc(hidden)]
#[instrument(skip(source))]
pub fn execute_with_source(path_db: &Path, args: StoreArgs, mut source: impl Read) -> Result<()> {
let StoreArgs {
max_entries,
max_entry_age: max_age,
max_entry_length: max_bytes,
min_entry_length: min_bytes,
store_sensitive,
ignore_pattern,
} = args;
if min_bytes > max_bytes {
bail!("minimum entry length ({min_bytes}) exceeds maximum entry length ({max_bytes})")
}
if let Ok(s) = std::env::var("CLIPBOARD_STATE") {
tracing::debug!("CLIPBOARD_STATE={s}");
match s.as_str() {
"sensitive" if !store_sensitive => {
tracing::trace!("sensitive - not storing");
return Ok(());
}
"clear" => {
tracing::debug!("explicitly cleared clipboard");
return delete_all_entries(&init_db(path_db)?);
}
"nil" => return Ok(()),
_ => {}
}
};
let buf = {
let mut buf = vec![];
source
.read_to_end(&mut buf)
.into_diagnostic()
.context("failed to read from STDIN")?;
buf
};
drop(source);
if buf.is_empty() {
tracing::trace!("no content to store");
return Ok(());
}
let gt_max = buf.len() > max_bytes && max_bytes != 0;
let lt_min = buf.len() < min_bytes;
if gt_max || lt_min {
tracing::debug!(
"content length ({}) is outside the bounds {min_bytes}->{max_bytes}",
buf.len()
);
return Ok(());
}
if buf.trim_ascii().is_empty() {
tracing::debug!("only ASCII whitespace content");
return Ok(());
}
if let Some(regexes) = ignore_pattern
&& matches!(
content_inspector::inspect(&buf),
ContentType::UTF_8 | ContentType::UTF_8_BOM
)
&& regexes
.iter()
.any(|re| re.is_match(&String::from_utf8_lossy(&buf)))
{
tracing::debug!("content matched an ignore pattern");
return Ok(());
}
let conn = &init_db(path_db)?;
let max_age = max_age.as_secs();
if max_age != 0 {
let timestamp = now() - max_age;
delete_entries_older_than(conn, timestamp)?;
}
let entry = {
let content_type = content_inspector::inspect(&buf);
let (mut mimetype, mut extra_preview_data) = (None, None);
if content_type.is_binary() {
if let Some((img_mimetype, img)) = decode_image(&buf) {
let (w, h) = img.dimensions();
extra_preview_data = Some(format!("{w}x{h}"));
mimetype = Some(img_mimetype.into());
}
else if let Some(content_mimetype) = get_mimetype(&buf) {
mimetype = Some(content_mimetype);
}
};
ClipboardEntry {
content: buf,
content_type: Some(content_type),
mimetype,
extra_preview_data,
..Default::default()
}
};
upsert_entry(conn, entry)?;
if max_entries != 0 {
trim_entries(conn, max_entries)?;
}
Ok(())
}