diff --git a/services/backup/src/constants.rs b/services/backup/src/constants.rs --- a/services/backup/src/constants.rs +++ b/services/backup/src/constants.rs @@ -20,7 +20,7 @@ pub const BACKUP_TABLE_FIELD_CREATED: &str = "created"; pub const BACKUP_TABLE_FIELD_USER_DATA: &str = "userData"; pub const BACKUP_TABLE_FIELD_USER_KEYS: &str = "userKeys"; -pub const BACKUP_TABLE_FIELD_ATTACHMENT_HOLDERS: &str = "attachmentHolders"; +pub const BACKUP_TABLE_FIELD_ATTACHMENTS: &str = "attachments"; pub const BACKUP_TABLE_INDEX_USERID_CREATED: &str = "userID-created-index"; pub const LOG_TABLE_NAME: &str = "backup-service-log"; diff --git a/services/backup/src/database/backup_item.rs b/services/backup/src/database/backup_item.rs --- a/services/backup/src/database/backup_item.rs +++ b/services/backup/src/database/backup_item.rs @@ -1,13 +1,13 @@ use aws_sdk_dynamodb::types::AttributeValue; use chrono::{DateTime, Utc}; use comm_services_lib::{ - blob::types::BlobInfo, - database::{DBItemError, TryFromAttribute}, + blob::{client::BlobServiceClient, types::BlobInfo}, + database::{AttributeTryInto, DBItemError, TryFromAttribute}, }; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use crate::constants::{ - BACKUP_TABLE_FIELD_ATTACHMENT_HOLDERS, BACKUP_TABLE_FIELD_BACKUP_ID, + BACKUP_TABLE_FIELD_ATTACHMENTS, BACKUP_TABLE_FIELD_BACKUP_ID, BACKUP_TABLE_FIELD_CREATED, BACKUP_TABLE_FIELD_USER_DATA, BACKUP_TABLE_FIELD_USER_ID, BACKUP_TABLE_FIELD_USER_KEYS, }; @@ -19,7 +19,7 @@ pub created: DateTime, pub user_keys: BlobInfo, pub user_data: BlobInfo, - pub attachment_holders: HashSet, + pub attachments: Vec, } impl BackupItem { @@ -28,7 +28,7 @@ backup_id: String, user_keys: BlobInfo, user_data: BlobInfo, - attachment_holders: HashSet, + attachments: Vec, ) -> Self { BackupItem { user_id, @@ -36,7 +36,22 @@ created: chrono::Utc::now(), user_keys, user_data, - attachment_holders, + attachments, + } + } + + pub async fn revoke_holders(self, blob_client: &BlobServiceClient) { + blob_client + .schedule_revoke_holder(self.user_keys.blob_hash, self.user_keys.holder); + + blob_client + .schedule_revoke_holder(self.user_data.blob_hash, self.user_data.holder); + + for attachment_info in self.attachments { + blob_client.schedule_revoke_holder( + attachment_info.blob_hash, + attachment_info.holder, + ); } } } @@ -66,10 +81,16 @@ ), ]); - if !value.attachment_holders.is_empty() { + if !value.attachments.is_empty() { attrs.insert( - BACKUP_TABLE_FIELD_ATTACHMENT_HOLDERS.to_string(), - AttributeValue::Ss(value.attachment_holders.into_iter().collect()), + BACKUP_TABLE_FIELD_ATTACHMENTS.to_string(), + AttributeValue::L( + value + .attachments + .into_iter() + .map(AttributeValue::from) + .collect(), + ), ); } @@ -105,14 +126,11 @@ value.remove(BACKUP_TABLE_FIELD_USER_DATA), )?; - let attachments = value.remove(BACKUP_TABLE_FIELD_ATTACHMENT_HOLDERS); - let attachment_holders = if attachments.is_some() { - HashSet::::try_from_attr( - BACKUP_TABLE_FIELD_ATTACHMENT_HOLDERS, - attachments, - )? + let attachments = value.remove(BACKUP_TABLE_FIELD_ATTACHMENTS); + let attachments = if attachments.is_some() { + attachments.attr_try_into(BACKUP_TABLE_FIELD_ATTACHMENTS)? } else { - HashSet::new() + Vec::new() }; Ok(BackupItem { @@ -121,7 +139,7 @@ created, user_keys, user_data, - attachment_holders, + attachments, }) } } diff --git a/services/backup/src/http/handlers/backup.rs b/services/backup/src/http/handlers/backup.rs --- a/services/backup/src/http/handlers/backup.rs +++ b/services/backup/src/http/handlers/backup.rs @@ -1,5 +1,3 @@ -use std::{collections::HashSet, convert::Infallible}; - use actix_web::{ error::ErrorBadRequest, web::{self, Bytes}, @@ -12,6 +10,7 @@ http::multipart::{get_named_text_field, get_text_field}, tools::Defer, }; +use std::convert::Infallible; use tokio_stream::{wrappers::ReceiverStream, StreamExt}; use tracing::{info, instrument, trace, warn}; @@ -49,7 +48,7 @@ ) .await?; - let attachments_holders: HashSet = + let attachments_hashes: Vec = match get_text_field(&mut multipart).await? { Some((name, attachments)) => { if name != "attachments" { @@ -62,15 +61,28 @@ attachments.lines().map(ToString::to_string).collect() } - None => HashSet::new(), + None => Vec::new(), }; + let mut attachments = Vec::new(); + let mut attachments_revokes = Vec::new(); + for attachment_hash in attachments_hashes { + let (holder, revoke) = + create_attachment_holder(&attachment_hash, &blob_client).await?; + + attachments.push(BlobInfo { + blob_hash: attachment_hash, + holder, + }); + attachments_revokes.push(revoke); + } + let item = BackupItem::new( user.user_id.clone(), backup_id, user_keys_blob_info, user_data_blob_info, - attachments_holders, + attachments, ); db_client @@ -80,21 +92,16 @@ user_keys_revoke.cancel(); user_data_revoke.cancel(); + for attachment_revoke in attachments_revokes { + attachment_revoke.cancel(); + } for backup in db_client .remove_old_backups(&user.user_id) .await .map_err(BackupError::from)? { - blob_client.schedule_revoke_holder( - backup.user_keys.blob_hash, - backup.user_keys.holder, - ); - - blob_client.schedule_revoke_holder( - backup.user_data.blob_hash, - backup.user_data.holder, - ); + backup.revoke_holders(&blob_client).await; } Ok(HttpResponse::Ok().finish()) @@ -173,6 +180,30 @@ Ok((blob_info, revoke_holder)) } +#[instrument(skip_all, name = "create_attachment_holder")] +async fn create_attachment_holder<'revoke, 'blob: 'revoke>( + attachment: &str, + blob_client: &'blob web::Data, +) -> Result<(String, Defer<'revoke>), BackupError> { + let holder = uuid::Uuid::new_v4().to_string(); + + if !blob_client + .assign_holder(attachment, &holder) + .await + .map_err(BackupError::from)? + { + warn!("Blob attachment with hash {attachment:?} doesn't exist"); + } + + let revoke_hash = attachment.to_string(); + let revoke_holder = holder.clone(); + let revoke_holder = Defer::new(|| { + blob_client.schedule_revoke_holder(revoke_hash, revoke_holder) + }); + + Ok((holder, revoke_holder)) +} + #[instrument(name = "download_user_keys", skip_all, fields(backup_id = %path.as_str()))] pub async fn download_user_keys( user: UserIdentity, diff --git a/services/comm-services-lib/src/database.rs b/services/comm-services-lib/src/database.rs --- a/services/comm-services-lib/src/database.rs +++ b/services/comm-services-lib/src/database.rs @@ -282,6 +282,35 @@ } } +impl TryFromAttribute for Vec { + fn try_from_attr( + attribute_name: impl Into, + attribute: Option, + ) -> Result { + let attribute_name = attribute_name.into(); + match attribute { + Some(AttributeValue::L(list)) => Ok( + list + .into_iter() + .map(|attribute| { + T::try_from_attr(format!("{attribute_name}[i]"), Some(attribute)) + }) + .collect::, _>>()?, + ), + Some(_) => Err(DBItemError::new( + attribute_name.into(), + Value::AttributeValue(attribute), + DBItemAttributeError::IncorrectType, + )), + None => Err(DBItemError::new( + attribute_name.into(), + Value::AttributeValue(attribute), + DBItemAttributeError::Missing, + )), + } + } +} + #[deprecated = "Use `String::try_from_attr()` instead"] pub fn parse_string_attribute( attribute_name: impl Into,