mirror of
https://github.com/NotAShelf/stash.git
synced 2026-04-13 14:33: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,
|
||||
out: impl Write,
|
||||
preview_width: u32,
|
||||
include_expired: bool,
|
||||
) -> Result<usize, StashError>;
|
||||
fn decode_entry(
|
||||
&self,
|
||||
|
|
@ -93,7 +94,6 @@ pub trait ClipboardDb {
|
|||
&self,
|
||||
id: i64,
|
||||
) -> Result<(i64, Vec<u8>, Option<String>), StashError>;
|
||||
fn next_sequence(&self) -> i64;
|
||||
}
|
||||
|
||||
#[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| {
|
||||
StashError::Store(
|
||||
format!("Failed to commit migration transaction: {e}").into(),
|
||||
|
|
@ -311,13 +354,17 @@ 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
|
||||
.conn
|
||||
.prepare(
|
||||
"SELECT id, contents, mime FROM clipboard ORDER BY \
|
||||
COALESCE(last_accessed, 0) DESC, id DESC",
|
||||
)
|
||||
.prepare(query)
|
||||
.map_err(|e| StashError::ListDecode(e.to_string().into()))?;
|
||||
let mut rows = stmt
|
||||
.query([])
|
||||
|
|
@ -526,13 +573,18 @@ impl ClipboardDb for SqliteClipboardDb {
|
|||
&self,
|
||||
mut out: impl Write,
|
||||
preview_width: u32,
|
||||
include_expired: bool,
|
||||
) -> 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
|
||||
.conn
|
||||
.prepare(
|
||||
"SELECT id, contents, mime FROM clipboard ORDER BY \
|
||||
COALESCE(last_accessed, 0) DESC, id DESC",
|
||||
)
|
||||
.prepare(query)
|
||||
.map_err(|e| StashError::ListDecode(e.to_string().into()))?;
|
||||
let mut rows = stmt
|
||||
.query([])
|
||||
|
|
@ -680,17 +732,6 @@ impl ClipboardDb for SqliteClipboardDb {
|
|||
|
||||
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 {
|
||||
|
|
@ -715,40 +756,6 @@ impl SqliteClipboardDb {
|
|||
.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(
|
||||
|
|
@ -778,6 +785,66 @@ impl SqliteClipboardDb {
|
|||
.map_err(|e| StashError::Store(e.to_string().into()))?;
|
||||
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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue