/* Opaque client-owned bytes. Confidentiality is provided by the client-side blob format. */ use crate::storage_error::StorageError; use crate::util::{config_util::CONFIG, db, sync}; use rusqlite::{Connection, OptionalExtension, Row, Transaction, params}; pub const MAX_BLOB_ID_BYTES: usize = 256; pub const MAX_BLOB_BYTES: usize = 1_048_576; pub const MAX_USER_BLOB_BYTES: usize = 16_777_216; #[derive(Debug, Clone, PartialEq, Eq)] pub struct UserBlob { pub id: i64, pub user_id: i64, pub blob_id: String, pub blob: Vec, pub revision: i64, pub updated_at: i64, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct UserBlobMetadata { pub blob_id: String, pub revision: i64, pub updated_at: i64, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct DeletedUserBlob { pub id: i64, pub blob_id: String, pub revision: i64, pub changed: bool, } fn validate(user_id: i64, blob_id: &str, blob: Option<&[u8]>) -> Result<(), StorageError> { if user_id <= 0 { return Err(StorageError::Other("invalid blob owner".into())); } if blob_id.is_empty() || blob_id.len() > MAX_BLOB_ID_BYTES { return Err(StorageError::Other("invalid blob id".into())); } if let Some(blob) = blob { if blob.len() > MAX_BLOB_BYTES { return Err(StorageError::Other("blob exceeds size limit".into())); } } Ok(()) } fn from_row(row: &Row<'_>) -> rusqlite::Result { Ok(UserBlob { id: row.get(0)?, user_id: row.get(1)?, blob_id: row.get(2)?, blob: row.get(3)?, revision: row.get(4)?, updated_at: row.get(5)?, }) } fn metadata_from_row(row: &Row<'_>) -> rusqlite::Result { Ok(UserBlobMetadata { blob_id: row.get(0)?, revision: row.get(1)?, updated_at: row.get(2)?, }) } fn load(tx: &Transaction<'_>, id: i64) -> Result { Ok(tx.query_row("SELECT id, user_id, blob_id, blob, revision, updated_at FROM user_blobs WHERE id = ?1 AND deleted = 0", [id], from_row)?) } pub fn put( user_id: i64, blob_id: &str, blob: &[u8], _expected_revision: Option, ) -> Result { validate(user_id, blob_id, Some(blob))?; db::with_immediate_transaction(|tx| { let existing = tx .query_row( "SELECT id, revision, deleted, length(blob) FROM user_blobs WHERE user_id = ?1 AND blob_id = ?2", params![user_id, blob_id], |r| { Ok(( r.get::<_, i64>(0)?, r.get::<_, i64>(1)?, r.get::<_, bool>(2)?, r.get::<_, i64>(3)?, )) }, ) .optional()?; let existing_id = existing.map(|(id, _, _, _)| id); let current_size = existing .filter(|(_, _, deleted, _)| !*deleted) .map(|(_, _, _, size)| size) .unwrap_or(0); if existing.is_none_or(|(_, _, deleted, _)| deleted) { let count: i64 = tx.query_row( "SELECT COUNT(*) FROM user_blobs WHERE user_id = ?1 AND deleted = 0", [user_id], |row| row.get(0), )?; if count >= CONFIG.load().storage_limits.max_user_blobs { return Err(StorageError::Other("user blob count quota exceeded".into())); } } let aggregate: i64 = tx.query_row("SELECT COALESCE(SUM(length(blob)), 0) FROM user_blobs WHERE user_id = ?1 AND deleted = 0", [user_id], |r| r.get(0))?; let proposed_size = aggregate .checked_sub(current_size) .and_then(|size| size.checked_add(blob.len() as i64)) .ok_or_else(|| StorageError::Other("user blob storage quota overflow".into()))?; if proposed_size > MAX_USER_BLOB_BYTES as i64 { return Err(StorageError::Other( "user blob storage limit exceeded".into(), )); } let id = match existing_id { Some(id) => id, None => { tx.execute("INSERT INTO user_blobs (user_id, blob_id, blob, revision, deleted, updated_at) VALUES (?1, ?2, ?3, 0, 0, ?4)", params![user_id, blob_id, blob, sync::now_millis()])?; tx.last_insert_rowid() } }; let revision = sync::record_event( tx, user_id, sync::EntityType::UserBlob, id, sync::Operation::Upsert, )?; tx.execute("UPDATE user_blobs SET blob = ?2, revision = ?3, deleted = 0, updated_at = ?4 WHERE id = ?1", params![id, blob, revision, sync::now_millis()])?; load(tx, id) }) } pub fn get(user_id: i64, blob_id: &str) -> Result, StorageError> { validate(user_id, blob_id, None)?; db::with_db(|conn| get_on(conn, user_id, blob_id)) } fn get_on( conn: &Connection, user_id: i64, blob_id: &str, ) -> Result, StorageError> { Ok(conn.query_row("SELECT id, user_id, blob_id, blob, revision, updated_at FROM user_blobs WHERE user_id = ?1 AND blob_id = ?2 AND deleted = 0", params![user_id, blob_id], from_row).optional()?) } pub fn list(user_id: i64) -> Result, StorageError> { list_by_query( user_id, "SELECT id, user_id, blob_id, blob, revision, updated_at FROM user_blobs WHERE user_id = ?1 AND deleted = 0", &[], ) } pub const BLOB_PAGE_SIZE: i64 = 128; pub fn list_metadata_page( user_id: i64, after_id: Option, ) -> Result<(Vec, Option), StorageError> { if user_id <= 0 || after_id.is_some_and(|id| id <= 0) { return Err(StorageError::Other("invalid blob list cursor".into())); } db::with_db(|conn| { let mut statement = conn.prepare("SELECT id, blob_id, revision, updated_at FROM user_blobs WHERE user_id = ?1 AND deleted = 0 AND id < ?2 ORDER BY id DESC LIMIT ?3")?; let mut rows = statement.query(params![ user_id, after_id.unwrap_or(i64::MAX), BLOB_PAGE_SIZE + 1 ])?; let mut items = Vec::new(); while let Some(row) = rows.next()? { items.push(( row.get::<_, i64>(0)?, UserBlobMetadata { blob_id: row.get(1)?, revision: row.get(2)?, updated_at: row.get(3)?, }, )); } let next = if items.len() > BLOB_PAGE_SIZE as usize { items.pop(); items.last().map(|(id, _)| *id) } else { None }; Ok(( items.into_iter().map(|(_, metadata)| metadata).collect(), next, )) }) } pub fn list_metadata(user_id: i64) -> Result, StorageError> { if user_id <= 0 { return Err(StorageError::Other("invalid blob owner".into())); } db::with_db(|conn| { let mut statement = conn.prepare( "SELECT blob_id, revision, updated_at FROM user_blobs WHERE user_id = ?1 AND deleted = 0", )?; let rows = statement.query_map([user_id], metadata_from_row)?; rows.collect::, _>>() .map_err(StorageError::from) }) } pub fn list_by_ids(user_id: i64, ids: &[i64]) -> Result, StorageError> { if user_id <= 0 { return Err(StorageError::Other("invalid blob owner".into())); } if ids.is_empty() { return Ok(Vec::new()); } db::with_db(|conn| { let mut out = Vec::new(); for id in ids { if let Some(blob) = conn.query_row("SELECT id, user_id, blob_id, blob, revision, updated_at FROM user_blobs WHERE user_id = ?1 AND id = ?2 AND deleted = 0", params![user_id, id], from_row).optional()? { out.push(blob); } } Ok(out) }) } pub fn list_deleted_by_ids( user_id: i64, ids: &[i64], ) -> Result, StorageError> { if user_id <= 0 { return Err(StorageError::Other("invalid blob owner".into())); } if ids.is_empty() { return Ok(Vec::new()); } db::with_db(|conn| { let mut deleted = Vec::new(); for id in ids { if let Some(blob) = conn.query_row( "SELECT id, blob_id, revision FROM user_blobs WHERE user_id = ?1 AND id = ?2 AND deleted = 1", params![user_id, id], |row| { Ok(DeletedUserBlob { id: row.get(0)?, blob_id: row.get(1)?, revision: row.get(2)?, changed: true, }) }, ).optional()? { deleted.push(blob); } } Ok(deleted) }) } fn list_by_query(user_id: i64, query: &str, _: &[i64]) -> Result, StorageError> { if user_id <= 0 { return Err(StorageError::Other("invalid blob owner".into())); } db::with_db(|conn| { let mut statement = conn.prepare(query)?; let rows = statement.query_map([user_id], from_row)?; rows.collect::, _>>() .map_err(StorageError::from) }) } pub fn delete( user_id: i64, blob_id: &str, _expected_revision: Option, ) -> Result, StorageError> { validate(user_id, blob_id, None)?; db::with_immediate_transaction(|tx| { let existing = tx .query_row( "SELECT id, revision, deleted FROM user_blobs WHERE user_id = ?1 AND blob_id = ?2", params![user_id, blob_id], |r| { Ok(( r.get::<_, i64>(0)?, r.get::<_, i64>(1)?, r.get::<_, bool>(2)?, )) }, ) .optional()?; let Some((id, current, deleted)) = existing else { return Ok(None); }; if deleted { return Ok(Some(DeletedUserBlob { id, blob_id: blob_id.into(), revision: current, changed: false, })); } let revision = sync::record_event( tx, user_id, sync::EntityType::UserBlob, id, sync::Operation::Delete, )?; tx.execute( "UPDATE user_blobs SET blob = X'', deleted = 1, revision = ?2, updated_at = ?3 WHERE id = ?1", params![id, revision, sync::now_millis()], )?; Ok(Some(DeletedUserBlob { id, blob_id: blob_id.into(), revision, changed: true, })) }) }