mirror of
https://github.com/NotAShelf/stash.git
synced 2026-04-13 06:23:47 +00:00
db: add is_expired column and implement vacuum/stats commands
Migrates schema to v5; `is_expired` column is added with partial index and `include_expired` parameter to `list_entries()` and `list_json()` methods. Also adds `vacuum()` and `stats()` methods for SQlite "administration", and removes `next_sequence()` from trait and impl. This has been a valuable addition to stash, as the database is now *less abstract* in the sense that user is made aware of its presence (stash wipe -> stash db wipe) and can modify it. Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Icfab67753d7f18e3798c0a930b16d05e6a6a6964
This commit is contained in:
parent
f4936e56ff
commit
d40b547c07
1 changed files with 122 additions and 55 deletions
177
src/db/mod.rs
177
src/db/mod.rs
|
|
@ -80,6 +80,7 @@ pub trait ClipboardDb {
|
||||||
&self,
|
&self,
|
||||||
out: impl Write,
|
out: impl Write,
|
||||||
preview_width: u32,
|
preview_width: u32,
|
||||||
|
include_expired: bool,
|
||||||
) -> Result<usize, StashError>;
|
) -> Result<usize, StashError>;
|
||||||
fn decode_entry(
|
fn decode_entry(
|
||||||
&self,
|
&self,
|
||||||
|
|
@ -93,7 +94,6 @@ pub trait ClipboardDb {
|
||||||
&self,
|
&self,
|
||||||
id: i64,
|
id: i64,
|
||||||
) -> Result<(i64, Vec<u8>, Option<String>), StashError>;
|
) -> Result<(i64, Vec<u8>, Option<String>), StashError>;
|
||||||
fn next_sequence(&self) -> i64;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
|
@ -296,6 +296,49 @@ impl SqliteClipboardDb {
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add is_expired column if it doesn't exist (v5)
|
||||||
|
if schema_version < 5 {
|
||||||
|
let has_is_expired: 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("is_expired"))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if !has_is_expired {
|
||||||
|
tx.execute(
|
||||||
|
"ALTER TABLE clipboard ADD COLUMN is_expired INTEGER DEFAULT 0",
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
.map_err(|e| {
|
||||||
|
StashError::Store(
|
||||||
|
format!("Failed to add is_expired column: {e}").into(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create index for is_expired (for filtering)
|
||||||
|
tx.execute(
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_is_expired ON clipboard(is_expired) \
|
||||||
|
WHERE is_expired = 1",
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
.map_err(|e| {
|
||||||
|
StashError::Store(
|
||||||
|
format!("Failed to create is_expired index: {e}").into(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
tx.execute("PRAGMA user_version = 5", []).map_err(|e| {
|
||||||
|
StashError::Store(format!("Failed to set schema version: {e}").into())
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
tx.commit().map_err(|e| {
|
tx.commit().map_err(|e| {
|
||||||
StashError::Store(
|
StashError::Store(
|
||||||
format!("Failed to commit migration transaction: {e}").into(),
|
format!("Failed to commit migration transaction: {e}").into(),
|
||||||
|
|
@ -311,13 +354,17 @@ impl SqliteClipboardDb {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SqliteClipboardDb {
|
impl SqliteClipboardDb {
|
||||||
pub fn list_json(&self) -> Result<String, StashError> {
|
pub fn list_json(&self, include_expired: bool) -> Result<String, StashError> {
|
||||||
|
let query = if include_expired {
|
||||||
|
"SELECT id, contents, mime FROM clipboard ORDER BY \
|
||||||
|
COALESCE(last_accessed, 0) DESC, id DESC"
|
||||||
|
} else {
|
||||||
|
"SELECT id, contents, mime FROM clipboard WHERE (is_expired IS NULL OR \
|
||||||
|
is_expired = 0) ORDER BY COALESCE(last_accessed, 0) DESC, id DESC"
|
||||||
|
};
|
||||||
let mut stmt = self
|
let mut stmt = self
|
||||||
.conn
|
.conn
|
||||||
.prepare(
|
.prepare(query)
|
||||||
"SELECT id, contents, mime FROM clipboard ORDER BY \
|
|
||||||
COALESCE(last_accessed, 0) DESC, id DESC",
|
|
||||||
)
|
|
||||||
.map_err(|e| StashError::ListDecode(e.to_string().into()))?;
|
.map_err(|e| StashError::ListDecode(e.to_string().into()))?;
|
||||||
let mut rows = stmt
|
let mut rows = stmt
|
||||||
.query([])
|
.query([])
|
||||||
|
|
@ -526,13 +573,18 @@ impl ClipboardDb for SqliteClipboardDb {
|
||||||
&self,
|
&self,
|
||||||
mut out: impl Write,
|
mut out: impl Write,
|
||||||
preview_width: u32,
|
preview_width: u32,
|
||||||
|
include_expired: bool,
|
||||||
) -> Result<usize, StashError> {
|
) -> Result<usize, StashError> {
|
||||||
|
let query = if include_expired {
|
||||||
|
"SELECT id, contents, mime FROM clipboard ORDER BY \
|
||||||
|
COALESCE(last_accessed, 0) DESC, id DESC"
|
||||||
|
} else {
|
||||||
|
"SELECT id, contents, mime FROM clipboard WHERE (is_expired IS NULL OR \
|
||||||
|
is_expired = 0) ORDER BY COALESCE(last_accessed, 0) DESC, id DESC"
|
||||||
|
};
|
||||||
let mut stmt = self
|
let mut stmt = self
|
||||||
.conn
|
.conn
|
||||||
.prepare(
|
.prepare(query)
|
||||||
"SELECT id, contents, mime FROM clipboard ORDER BY \
|
|
||||||
COALESCE(last_accessed, 0) DESC, id DESC",
|
|
||||||
)
|
|
||||||
.map_err(|e| StashError::ListDecode(e.to_string().into()))?;
|
.map_err(|e| StashError::ListDecode(e.to_string().into()))?;
|
||||||
let mut rows = stmt
|
let mut rows = stmt
|
||||||
.query([])
|
.query([])
|
||||||
|
|
@ -680,17 +732,6 @@ impl ClipboardDb for SqliteClipboardDb {
|
||||||
|
|
||||||
Ok((id, contents, mime))
|
Ok((id, contents, mime))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn next_sequence(&self) -> i64 {
|
|
||||||
match self
|
|
||||||
.conn
|
|
||||||
.query_row("SELECT MAX(id) FROM clipboard", [], |row| {
|
|
||||||
row.get::<_, Option<i64>>(0)
|
|
||||||
}) {
|
|
||||||
Ok(Some(max_id)) => max_id + 1,
|
|
||||||
Ok(None) | Err(_) => 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SqliteClipboardDb {
|
impl SqliteClipboardDb {
|
||||||
|
|
@ -715,40 +756,6 @@ impl SqliteClipboardDb {
|
||||||
.map_err(|e| StashError::Trim(e.to_string().into()))
|
.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
|
/// Get the earliest expiration (timestamp, id) for heap initialization
|
||||||
pub fn get_next_expiration(&self) -> Result<Option<(f64, i64)>, StashError> {
|
pub fn get_next_expiration(&self) -> Result<Option<(f64, i64)>, StashError> {
|
||||||
match self.conn.query_row(
|
match self.conn.query_row(
|
||||||
|
|
@ -778,6 +785,66 @@ impl SqliteClipboardDb {
|
||||||
.map_err(|e| StashError::Store(e.to_string().into()))?;
|
.map_err(|e| StashError::Store(e.to_string().into()))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Optimize database using VACUUM
|
||||||
|
pub fn vacuum(&self) -> Result<(), StashError> {
|
||||||
|
self
|
||||||
|
.conn
|
||||||
|
.execute("VACUUM", [])
|
||||||
|
.map_err(|e| StashError::Store(e.to_string().into()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get database statistics
|
||||||
|
pub fn stats(&self) -> Result<String, StashError> {
|
||||||
|
let total: i64 = self
|
||||||
|
.conn
|
||||||
|
.query_row("SELECT COUNT(*) FROM clipboard", [], |row| row.get(0))
|
||||||
|
.map_err(|e| StashError::ListDecode(e.to_string().into()))?;
|
||||||
|
|
||||||
|
let expired: i64 = self
|
||||||
|
.conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT COUNT(*) FROM clipboard WHERE is_expired = 1",
|
||||||
|
[],
|
||||||
|
|row| row.get(0),
|
||||||
|
)
|
||||||
|
.map_err(|e| StashError::ListDecode(e.to_string().into()))?;
|
||||||
|
|
||||||
|
let active = total - expired;
|
||||||
|
|
||||||
|
let with_expiration: i64 = self
|
||||||
|
.conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT COUNT(*) FROM clipboard WHERE expires_at IS NOT NULL AND \
|
||||||
|
(is_expired IS NULL OR is_expired = 0)",
|
||||||
|
[],
|
||||||
|
|row| row.get(0),
|
||||||
|
)
|
||||||
|
.map_err(|e| StashError::ListDecode(e.to_string().into()))?;
|
||||||
|
|
||||||
|
// Get database file size
|
||||||
|
let page_count: i64 = self
|
||||||
|
.conn
|
||||||
|
.query_row("PRAGMA page_count", [], |row| row.get(0))
|
||||||
|
.map_err(|e| StashError::ListDecode(e.to_string().into()))?;
|
||||||
|
|
||||||
|
let page_size: i64 = self
|
||||||
|
.conn
|
||||||
|
.query_row("PRAGMA page_size", [], |row| row.get(0))
|
||||||
|
.map_err(|e| StashError::ListDecode(e.to_string().into()))?;
|
||||||
|
|
||||||
|
let size_bytes = page_count * page_size;
|
||||||
|
let size_mb = size_bytes as f64 / 1024.0 / 1024.0;
|
||||||
|
|
||||||
|
Ok(format!(
|
||||||
|
"Database Statistics:\n\nEntries:\nTotal: {total}\nActive: \
|
||||||
|
{active}\nExpired: {expired}\nWith TTL: \
|
||||||
|
{with_expiration}\n\nStorage:\nSize: {size_mb:.2} MB \
|
||||||
|
({size_bytes} bytes)\nPages: {page_count}\nPage size: \
|
||||||
|
{page_size} bytes"
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try to load a sensitive regex from systemd credential or env.
|
/// Try to load a sensitive regex from systemd credential or env.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue