mirror of
https://github.com/NotAShelf/stash.git
synced 2026-04-12 22:17:41 +00:00
db: add `expires_at column and expiration management methods
Schema v4: add expires_at REAL column with partial index for NULL values Other relevant methods that were added: - `now()` for Unix timestamp with sub-second precision - `cleanup_expired()` to remove all expired entries - `get_expired_entries()` for for diagnostic output (`stash list --expired`) - `get_next_expiration()` for heap initialization - `set_expiration()` to update expiration timestamp This feature has proven larger than I had anticipated (and hoped) but that's the reality of dealing with databases. Some of the methods are slightly redundant but it helps keep tracing the code manageable and semantically correct. We'll probably not regret those later. Probably. Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Ie9e5b0767673e74389b8e59c466afd946a6a6964
This commit is contained in:
parent
bb1c5dc50b
commit
71fc1ff40f
3 changed files with 150 additions and 15 deletions
35
Cargo.lock
generated
35
Cargo.lock
generated
|
|
@ -1011,6 +1011,12 @@ version = "0.4.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "humantime"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424"
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
|
|
@ -1121,9 +1127,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.85"
|
||||
version = "0.3.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
|
||||
checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
|
|
@ -2128,6 +2134,7 @@ dependencies = [
|
|||
"ctrlc",
|
||||
"dirs",
|
||||
"env_logger",
|
||||
"humantime",
|
||||
"imagesize",
|
||||
"inquire",
|
||||
"libc",
|
||||
|
|
@ -2561,18 +2568,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
|||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.2+wasi-0.2.9"
|
||||
version = "1.0.1+wasi-0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
|
||||
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.108"
|
||||
version = "0.2.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
|
||||
checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
|
|
@ -2583,9 +2590,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.108"
|
||||
version = "0.2.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
|
||||
checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
|
|
@ -2593,9 +2600,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.108"
|
||||
version = "0.2.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
|
||||
checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
|
|
@ -2606,9 +2613,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.108"
|
||||
version = "0.2.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
|
||||
checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
|
@ -2924,9 +2931,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
version = "0.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
|
||||
|
||||
[[package]]
|
||||
name = "wl-clipboard-rs"
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ repository = "https://github.com/notashelf/stash"
|
|||
rust-version = "1.90"
|
||||
|
||||
[[bin]]
|
||||
name = "stash" # actual binary name for Nix, Cargo, etc.
|
||||
name = "stash" # actual binary name for Nix, Cargo, etc.
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
|
|
@ -22,6 +22,7 @@ crossterm = "0.29.0"
|
|||
ctrlc = "3.5.1"
|
||||
dirs = "6.0.0"
|
||||
env_logger = "0.11.8"
|
||||
humantime = "2.3.0"
|
||||
imagesize = "0.14.0"
|
||||
inquire = { version = "0.9.2", default-features = false, features = [
|
||||
"crossterm",
|
||||
|
|
|
|||
127
src/db/mod.rs
127
src/db/mod.rs
|
|
@ -256,6 +256,46 @@ impl SqliteClipboardDb {
|
|||
})?;
|
||||
}
|
||||
|
||||
// Add expires_at column if it doesn't exist (v4)
|
||||
if schema_version < 4 {
|
||||
let has_expires_at: bool = tx
|
||||
.query_row(
|
||||
"SELECT sql FROM sqlite_master WHERE type='table' AND \
|
||||
name='clipboard'",
|
||||
[],
|
||||
|row| {
|
||||
let sql: String = row.get(0)?;
|
||||
Ok(sql.to_lowercase().contains("expires_at"))
|
||||
},
|
||||
)
|
||||
.unwrap_or(false);
|
||||
|
||||
if !has_expires_at {
|
||||
tx.execute("ALTER TABLE clipboard ADD COLUMN expires_at REAL", [])
|
||||
.map_err(|e| {
|
||||
StashError::Store(
|
||||
format!("Failed to add expires_at column: {e}").into(),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
// Create partial index for expires_at (only index non-NULL values)
|
||||
tx.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_expires_at ON clipboard(expires_at) \
|
||||
WHERE expires_at IS NOT NULL",
|
||||
[],
|
||||
)
|
||||
.map_err(|e| {
|
||||
StashError::Store(
|
||||
format!("Failed to create expires_at index: {e}").into(),
|
||||
)
|
||||
})?;
|
||||
|
||||
tx.execute("PRAGMA user_version = 4", []).map_err(|e| {
|
||||
StashError::Store(format!("Failed to set schema version: {e}").into())
|
||||
})?;
|
||||
}
|
||||
|
||||
tx.commit().map_err(|e| {
|
||||
StashError::Store(
|
||||
format!("Failed to commit migration transaction: {e}").into(),
|
||||
|
|
@ -653,6 +693,93 @@ impl ClipboardDb for SqliteClipboardDb {
|
|||
}
|
||||
}
|
||||
|
||||
impl SqliteClipboardDb {
|
||||
/// Get current Unix timestamp with sub-second precision
|
||||
pub fn now() -> f64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs_f64()
|
||||
}
|
||||
|
||||
/// Clean up all expired entries. Returns count deleted.
|
||||
pub fn cleanup_expired(&self) -> Result<usize, StashError> {
|
||||
let now = Self::now();
|
||||
self
|
||||
.conn
|
||||
.execute(
|
||||
"DELETE FROM clipboard WHERE expires_at IS NOT NULL AND expires_at <= \
|
||||
?1",
|
||||
[now],
|
||||
)
|
||||
.map_err(|e| StashError::Trim(e.to_string().into()))
|
||||
}
|
||||
|
||||
/// Get expired entries (for --expired flag diagnostic output)
|
||||
pub fn get_expired_entries(
|
||||
&self,
|
||||
) -> Result<Vec<(i64, Vec<u8>, Option<String>)>, StashError> {
|
||||
let now = Self::now();
|
||||
let mut stmt = self
|
||||
.conn
|
||||
.prepare(
|
||||
"SELECT id, contents, mime FROM clipboard WHERE expires_at IS NOT \
|
||||
NULL AND expires_at <= ?1 ORDER BY expires_at ASC",
|
||||
)
|
||||
.map_err(|e| StashError::ListDecode(e.to_string().into()))?;
|
||||
let mut rows = stmt
|
||||
.query([now])
|
||||
.map_err(|e| StashError::ListDecode(e.to_string().into()))?;
|
||||
let mut entries = Vec::new();
|
||||
while let Some(row) = rows
|
||||
.next()
|
||||
.map_err(|e| StashError::ListDecode(e.to_string().into()))?
|
||||
{
|
||||
let id: i64 = row
|
||||
.get(0)
|
||||
.map_err(|e| StashError::ListDecode(e.to_string().into()))?;
|
||||
let contents: Vec<u8> = row
|
||||
.get(1)
|
||||
.map_err(|e| StashError::ListDecode(e.to_string().into()))?;
|
||||
let mime: Option<String> = row
|
||||
.get(2)
|
||||
.map_err(|e| StashError::ListDecode(e.to_string().into()))?;
|
||||
entries.push((id, contents, mime));
|
||||
}
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
/// Get the earliest expiration (timestamp, id) for heap initialization
|
||||
pub fn get_next_expiration(&self) -> Result<Option<(f64, i64)>, StashError> {
|
||||
match self.conn.query_row(
|
||||
"SELECT expires_at, id FROM clipboard WHERE expires_at IS NOT NULL \
|
||||
ORDER BY expires_at ASC LIMIT 1",
|
||||
[],
|
||||
|row| Ok((row.get(0)?, row.get(1)?)),
|
||||
) {
|
||||
Ok(result) => Ok(Some(result)),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||
Err(e) => Err(StashError::Store(e.to_string().into())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set expiration timestamp for an entry
|
||||
pub fn set_expiration(
|
||||
&self,
|
||||
id: i64,
|
||||
expires_at: f64,
|
||||
) -> Result<(), StashError> {
|
||||
self
|
||||
.conn
|
||||
.execute(
|
||||
"UPDATE clipboard SET expires_at = ?2 WHERE id = ?1",
|
||||
params![id, expires_at],
|
||||
)
|
||||
.map_err(|e| StashError::Store(e.to_string().into()))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to load a sensitive regex from systemd credential or env.
|
||||
///
|
||||
/// # Returns
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue