mirror of
https://github.com/NotAShelf/stash.git
synced 2026-04-13 14:33:47 +00:00
Merge pull request #4 from NotAShelf/notashelf/push-pvkyrosxlwkp
treewide: general cleanup
This commit is contained in:
commit
9c26b6e8d0
11 changed files with 383 additions and 206 deletions
104
.github/workflows/release.yaml
vendored
Normal file
104
.github/workflows/release.yaml
vendored
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
name: Publish Built Binaries
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
release_id: ${{ steps.create_release.outputs.id }}
|
||||
steps:
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
draft: false
|
||||
prerelease: false
|
||||
generate_release_notes: true
|
||||
|
||||
build-release:
|
||||
needs: create-release
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-gnu
|
||||
name: stash-linux-amd64
|
||||
cross: false
|
||||
- os: ubuntu-latest
|
||||
target: aarch64-unknown-linux-gnu
|
||||
name: stash-linux-arm64
|
||||
cross: true
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Setup cross-compilation (Linux ARM64)
|
||||
if: matrix.cross && matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gcc-aarch64-linux-gnu
|
||||
|
||||
- name: Install cross
|
||||
if: matrix.cross
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cross
|
||||
|
||||
- name: Build binary (native)
|
||||
if: ${{ !matrix.cross }}
|
||||
run: cargo build --release --target ${{ matrix.target }}
|
||||
|
||||
- name: Build binary (cross)
|
||||
if: ${{ matrix.cross }}
|
||||
run: cross build --release --target ${{ matrix.target }}
|
||||
|
||||
- name: Prepare binary (Unix)
|
||||
if: ${{ !contains(matrix.os, 'windows') }}
|
||||
run: |
|
||||
cp target/${{ matrix.target }}/release/stash ${{ matrix.name }}
|
||||
|
||||
- name: Upload Release Asset
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: ${{ matrix.name }}
|
||||
|
||||
generate-checksums:
|
||||
needs: [create-release, build-release]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Download Assets
|
||||
uses: robinraju/release-downloader@v1
|
||||
with:
|
||||
tag: ${{ github.ref_name }}
|
||||
fileName: "stash-*"
|
||||
out-file-path: "."
|
||||
|
||||
- name: Generate checksums
|
||||
run: |
|
||||
sha256sum stash-* > SHA256SUMS
|
||||
|
||||
- name: Upload Checksums
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
files: SHA256SUMS
|
||||
|
||||
12
Cargo.lock
generated
12
Cargo.lock
generated
|
|
@ -225,6 +225,16 @@ dependencies = [
|
|||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap-verbosity-flag"
|
||||
version = "3.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eeab6a5cdfc795a05538422012f20a5496f050223c91be4e5420bfd13c641fb1"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.44"
|
||||
|
|
@ -1192,6 +1202,7 @@ name = "stash"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"clap-verbosity-flag",
|
||||
"dirs",
|
||||
"env_logger",
|
||||
"image",
|
||||
|
|
@ -1199,6 +1210,7 @@ dependencies = [
|
|||
"rmp-serde",
|
||||
"serde",
|
||||
"sled",
|
||||
"thiserror 2.0.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -17,3 +17,5 @@ rmp-serde = "1.3.0"
|
|||
image = "0.25.6"
|
||||
log = "0.4.27"
|
||||
env_logger = "0.11.8"
|
||||
clap-verbosity-flag = "3.0.3"
|
||||
thiserror = "2.0.14"
|
||||
|
|
|
|||
|
|
@ -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<String>);
|
||||
fn decode(
|
||||
&self,
|
||||
in_: impl Read,
|
||||
out: impl Write,
|
||||
input: Option<String>,
|
||||
) -> Result<(), StashError>;
|
||||
}
|
||||
|
||||
impl DecodeCommand for SledClipboardDb {
|
||||
fn decode(&self, in_: impl Read, out: impl Write, input: Option<String>) {
|
||||
self.decode_entry(in_, out, input);
|
||||
fn decode(
|
||||
&self,
|
||||
in_: impl Read,
|
||||
out: impl Write,
|
||||
input: Option<String>,
|
||||
) -> Result<(), StashError> {
|
||||
self.decode_entry(in_, out, input)?;
|
||||
log::info!("Entry decoded");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<usize, StashError>;
|
||||
}
|
||||
|
||||
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<usize, StashError> {
|
||||
match self.delete_entries(input) {
|
||||
Ok(deleted) => {
|
||||
log::info!("Deleted {deleted} entries");
|
||||
Ok(deleted)
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to delete entries: {e}");
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<usize, StashError>;
|
||||
}
|
||||
|
||||
impl QueryCommand for SledClipboardDb {
|
||||
fn query_delete(&self, query: &str) {
|
||||
<SledClipboardDb as ClipboardDb>::delete_query(self, query);
|
||||
log::info!("Entries matching query '{}' deleted", query);
|
||||
fn query_delete(&self, query: &str) -> Result<usize, StashError> {
|
||||
<SledClipboardDb as ClipboardDb>::delete_query(self, query)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ pub trait StoreCommand {
|
|||
max_dedupe_search: u64,
|
||||
max_items: u64,
|
||||
state: Option<String>,
|
||||
);
|
||||
) -> 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<String>,
|
||||
) {
|
||||
) -> 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(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
291
src/db/mod.rs
291
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<String>);
|
||||
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<u64, StashError>;
|
||||
fn deduplicate(&self, buf: &[u8], max: u64) -> Result<usize, StashError>;
|
||||
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<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;
|
||||
}
|
||||
|
||||
|
|
@ -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<u64, StashError> {
|
||||
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,165 @@ 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<usize, StashError> {
|
||||
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(usize::try_from(max).unwrap_or(usize::MAX))
|
||||
{
|
||||
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}"),
|
||||
}
|
||||
for k in keys.drain(usize::try_from(max).unwrap_or(0)..) {
|
||||
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<usize, StashError> {
|
||||
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<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 {
|
||||
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(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<usize, StashError> {
|
||||
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<usize, StashError> {
|
||||
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 {
|
||||
|
|
@ -288,7 +301,9 @@ pub fn u64_to_ivec(v: u64) -> IVec {
|
|||
}
|
||||
|
||||
pub fn ivec_to_u64(v: &IVec) -> u64 {
|
||||
let arr: [u8; 8] = if let Ok(arr) = v.as_ref().try_into() { arr } else {
|
||||
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;
|
||||
};
|
||||
|
|
@ -351,7 +366,7 @@ 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 = size as f64;
|
||||
let mut fsize = f64::from(u32::try_from(size).unwrap_or(u32::MAX));
|
||||
let mut i = 0;
|
||||
while fsize >= 1024.0 && i < units.len() - 1 {
|
||||
fsize /= 1024.0;
|
||||
|
|
|
|||
109
src/main.rs
109
src/main.rs
|
|
@ -38,8 +38,8 @@ struct Cli {
|
|||
#[arg(long)]
|
||||
db_path: Option<PathBuf>,
|
||||
|
||||
#[arg(long)]
|
||||
import_tsv: bool,
|
||||
#[command(flatten)]
|
||||
verbosity: clap_verbosity_flag::Verbosity,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
|
|
@ -75,9 +75,22 @@ enum Command {
|
|||
},
|
||||
}
|
||||
|
||||
fn report_error<T>(result: Result<T, impl std::fmt::Display>, context: &str) -> Option<T> {
|
||||
match result {
|
||||
Ok(val) => Some(val),
|
||||
Err(e) => {
|
||||
log::error!("{context}: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
let cli = Cli::parse();
|
||||
env_logger::Builder::new()
|
||||
.filter_level(cli.verbosity.into())
|
||||
.init();
|
||||
|
||||
let db_path = cli.db_path.unwrap_or_else(|| {
|
||||
dirs::cache_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("/tmp"))
|
||||
|
|
@ -89,63 +102,67 @@ fn main() {
|
|||
log::error!("Failed to open database: {e}");
|
||||
process::exit(1);
|
||||
});
|
||||
let db = db::SledClipboardDb { db: sled_db };
|
||||
|
||||
if cli.import_tsv {
|
||||
db.import_tsv(io::stdin());
|
||||
return;
|
||||
}
|
||||
let db = db::SledClipboardDb { db: sled_db };
|
||||
|
||||
match cli.command {
|
||||
Some(Command::Store) => {
|
||||
log::info!("Executing: Store");
|
||||
let state = env::var("CLIPBOARD_STATE").ok();
|
||||
db.store(io::stdin(), cli.max_dedupe_search, cli.max_items, state);
|
||||
let state = env::var("STASH_CLIPBOARD_STATE").ok();
|
||||
report_error(
|
||||
db.store(io::stdin(), cli.max_dedupe_search, cli.max_items, state),
|
||||
"Failed to store entry",
|
||||
);
|
||||
}
|
||||
Some(Command::List) => {
|
||||
log::info!("Executing: List");
|
||||
db.list(io::stdout(), cli.preview_width);
|
||||
report_error(
|
||||
db.list(io::stdout(), cli.preview_width),
|
||||
"Failed to list entries",
|
||||
);
|
||||
}
|
||||
Some(Command::Decode { input }) => {
|
||||
log::info!("Executing: Decode");
|
||||
db.decode(io::stdin(), io::stdout(), input);
|
||||
report_error(
|
||||
db.decode(io::stdin(), io::stdout(), input),
|
||||
"Failed to decode entry",
|
||||
);
|
||||
}
|
||||
Some(Command::Delete { arg, r#type }) => {
|
||||
log::info!("Executing: Delete");
|
||||
match (arg, r#type.as_deref()) {
|
||||
(Some(s), Some("id")) => {
|
||||
if let Ok(id) = s.parse::<u64>() {
|
||||
use std::io::Cursor;
|
||||
db.delete(Cursor::new(format!("{id}\n")));
|
||||
} else {
|
||||
log::error!("Argument is not a valid id");
|
||||
}
|
||||
}
|
||||
(Some(s), Some("query")) => {
|
||||
db.query_delete(&s);
|
||||
}
|
||||
(Some(s), None) => {
|
||||
if let Ok(id) = s.parse::<u64>() {
|
||||
use std::io::Cursor;
|
||||
db.delete(Cursor::new(format!("{id}\n")));
|
||||
} else {
|
||||
db.query_delete(&s);
|
||||
}
|
||||
}
|
||||
(None, _) => {
|
||||
db.delete(io::stdin());
|
||||
}
|
||||
(_, Some(_)) => {
|
||||
log::error!("Unknown type for --type. Use \"id\" or \"query\".");
|
||||
Some(Command::Delete { arg, r#type }) => match (arg, r#type.as_deref()) {
|
||||
(Some(s), Some("id")) => {
|
||||
if let Ok(id) = s.parse::<u64>() {
|
||||
use std::io::Cursor;
|
||||
report_error(
|
||||
db.delete(Cursor::new(format!("{id}\n"))),
|
||||
"Failed to delete entry by id",
|
||||
);
|
||||
} else {
|
||||
log::error!("Argument is not a valid id");
|
||||
}
|
||||
}
|
||||
}
|
||||
(Some(s), Some("query")) => {
|
||||
report_error(db.query_delete(&s), "Failed to delete entry by query");
|
||||
}
|
||||
(Some(s), None) => {
|
||||
if let Ok(id) = s.parse::<u64>() {
|
||||
use std::io::Cursor;
|
||||
report_error(
|
||||
db.delete(Cursor::new(format!("{id}\n"))),
|
||||
"Failed to delete entry by id",
|
||||
);
|
||||
} else {
|
||||
report_error(db.query_delete(&s), "Failed to delete entry by query");
|
||||
}
|
||||
}
|
||||
(None, _) => {
|
||||
report_error(db.delete(io::stdin()), "Failed to delete entry from stdin");
|
||||
}
|
||||
(_, Some(_)) => {
|
||||
log::error!("Unknown type for --type. Use \"id\" or \"query\".");
|
||||
}
|
||||
},
|
||||
Some(Command::Wipe) => {
|
||||
log::info!("Executing: Wipe");
|
||||
db.wipe();
|
||||
report_error(db.wipe(), "Failed to wipe database");
|
||||
}
|
||||
|
||||
Some(Command::Import { r#type }) => {
|
||||
log::info!("Executing: Import");
|
||||
// Default format is TSV (Cliphist compatible)
|
||||
let format = r#type.as_deref().unwrap_or("tsv");
|
||||
match format {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue