diff --git a/services/reports/src/database/item.rs b/services/reports/src/database/item.rs --- a/services/reports/src/database/item.rs +++ b/services/reports/src/database/item.rs @@ -1,7 +1,11 @@ use aws_sdk_dynamodb::{primitives::Blob, types::AttributeValue}; use chrono::{DateTime, Utc}; use comm_services_lib::{ - blob::types::BlobInfo, + blob::{ + client::{BlobServiceClient, BlobServiceError}, + types::BlobInfo, + }, + constants::DDB_ITEM_SIZE_LIMIT, database::{ self, AttributeMap, AttributeTryInto, DBItemError, TryFromAttribute, }, @@ -30,6 +34,20 @@ pub encryption_key: Option, } +/// contains some redundancy as not all keys are always present +static REPORT_ITEM_KEYS_SIZE: usize = { + let mut size: usize = 0; + size += ATTR_REPORT_ID.as_bytes().len(); + size += ATTR_REPORT_TYPE.as_bytes().len(); + size += ATTR_USER_ID.as_bytes().len(); + size += ATTR_PLATFORM.as_bytes().len(); + size += ATTR_CREATION_TIME.as_bytes().len(); + size += ATTR_ENCRYPTION_KEY.as_bytes().len(); + size += ATTR_BLOB_INFO.as_bytes().len(); + size += ATTR_REPORT_CONTENT.as_bytes().len(); + size +}; + impl ReportItem { pub fn into_attrs(self) -> AttributeMap { let creation_time = self @@ -56,7 +74,45 @@ attrs } + pub async fn ensure_size_constraints( + &mut self, + blob_client: &BlobServiceClient, + ) -> Result<(), BlobServiceError> { + if self.total_size() < DDB_ITEM_SIZE_LIMIT { + return Ok(()); + }; + + self.content.move_to_blob(blob_client).await + } + + fn total_size(&self) -> usize { + let mut size = REPORT_ITEM_KEYS_SIZE; + size += self.id.as_bytes().len(); + size += self.user_id.as_bytes().len(); + size += self.platform.to_string().as_bytes().len(); + size += (self.report_type as u8).to_string().as_bytes().len(); + size += match &self.content { + ReportContent::Database(data) => data.len(), + ReportContent::Blob(info) => { + let mut blob_size = 0; + blob_size += "holder".as_bytes().len(); + blob_size += "blob_hash".as_bytes().len(); + blob_size += info.holder.as_bytes().len(); + blob_size += info.blob_hash.as_bytes().len(); + blob_size + } + }; + if let Some(key) = self.encryption_key.as_ref() { + size += key.as_bytes().len(); + } + size + } + /// Creates a report item from a report input payload + /// + /// WARN: Note that this method stores content as [`ReportStorage::Database`] + /// regardless of its size. Use [`ensure_size_constraints`] to move content to + /// blob storage if necessary. pub fn from_input( payload: ReportInput, user_id: Option, @@ -153,6 +209,16 @@ .attr_try_into(ATTR_REPORT_CONTENT)?; Ok(ReportContent::Database(content_data)) } + + /// Moves report content to blob storage: + /// - Switches `self` from [`ReportStorage::Database`] to [`ReportStorage::Blob`] + /// - No-op for [`ReportStorage::Blob`] + async fn move_to_blob( + &mut self, + blob_client: &BlobServiceClient, + ) -> Result<(), BlobServiceError> { + todo!() + } } // DB conversions for report types