//! Binary blob storage in the message-log `SQLite` database (e.g. images, //! attachments), keyed by session id and an arbitrary blob key. use anyhow::Result; use rusqlite::{params, Connection}; /// Insert or overwrite a blob for a session under `blob_key`. /// /// Flow: compute current timestamp -> `INSERT OR REPLACE` into `blobs` /// keyed on `(session_id, blob_key)`. /// /// `INSERT OR REPLACE` is used so re-uploading the same key overwrites /// the previous blob rather than failing on the UNIQUE constraint. /// /// Return: `Ok(())` on success, or the underlying `SQLite` error. pub fn store_blob( conn: &Connection, session_id: &str, blob_key: &str, data: &[u8], mime_type: Option<&str>, ) -> Result<()> { let created_at = chrono::Utc::now().timestamp_millis(); tracing::debug!(%session_id, %blob_key, size = data.len(), "store_blob"); conn.execute( "INSERT OR REPLACE INTO blobs (session_id, blob_key, data, mime_type, created_at) VALUES (?1, ?2, ?3, ?4, ?5)", params![session_id, blob_key, data, mime_type, created_at], )?; tracing::info!(%session_id, %blob_key, "store_blob — stored"); Ok(()) } /// Fetch a blob's bytes for a session by key. /// /// Return: `Ok(Some(data))` if found, `Ok(None)` if no matching row /// exists, `Err` for any other `SQLite` failure. pub fn retrieve_blob( conn: &Connection, session_id: &str, blob_key: &str, ) -> Result>> { tracing::debug!(%session_id, %blob_key, "retrieve_blob"); let result = conn.query_row( "SELECT data FROM blobs WHERE session_id = ?1 AND blob_key = ?2", params![session_id, blob_key], |row| row.get::<_, Vec>(0), ); match result { Ok(data) => { tracing::debug!(%session_id, %blob_key, size = data.len(), "retrieve_blob — found"); Ok(Some(data)) } Err(rusqlite::Error::QueryReturnedNoRows) => { tracing::debug!(%session_id, %blob_key, "retrieve_blob — not found"); Ok(None) } Err(e) => { tracing::error!(%session_id, %blob_key, error = %e, "retrieve_blob — query failed"); Err(e.into()) } } } /// List all blob keys stored for a session, oldest first. /// /// Return: `Ok(Vec)` of keys ordered by `created_at`, or the /// underlying `SQLite` error. pub fn list_blob_keys(conn: &Connection, session_id: &str) -> Result> { tracing::debug!(%session_id, "list_blob_keys"); let mut stmt = conn.prepare("SELECT blob_key FROM blobs WHERE session_id = ?1 ORDER BY created_at ASC")?; let rows = stmt.query_map(params![session_id], |row| row.get::<_, String>(0))?; let mut keys = Vec::new(); for row in rows { keys.push(row?); } tracing::debug!(%session_id, count = keys.len(), "list_blob_keys — done"); Ok(keys) }