feat(settings): add synced user assets

This commit is contained in:
Alois 2026-09-19 23:04:22 +02:00
commit 01f1834e25
Signed by: alois
SSH key fingerprint: SHA256:GBzT2DXvAuGV9XIV5W3WrzVpjU54FThmxHXdbz95J24
24 changed files with 959 additions and 50 deletions

View file

@ -5,6 +5,7 @@ use iota_storage::util::communities_util::CommunitiesUtil;
use iota_storage::util::e2ee_storage::{self, ChatSecretQuery};
use iota_storage::util::settings;
use iota_storage::util::synced_settings::{self, SettingScope, SyncedSetting};
use iota_storage::util::user_assets::{self, UploadStatus, UserAssetMetadata};
use iota_storage::util::user_blobs::{self, UserBlob, UserBlobMetadata};
use iota_storage::util::{blocked_users, message_storage_policy, receipt_policy};
use mtp::codec::{
@ -42,6 +43,8 @@ pub struct BlobMutation {
pub changed: Option<CommunicationValue>,
}
pub type AssetMutation = BlobMutation;
#[derive(Debug)]
pub struct PolicyMutation {
pub response: CommunicationValue,
@ -1937,6 +1940,109 @@ fn blob_request(cv: &CommunicationValue) -> Result<(i64, String), CommunicationV
Ok((user_id, id.to_owned()))
}
fn asset_metadata_value(asset: &UserAssetMetadata) -> DataValue {
typed_container(vec![
(DataType::AssetId, DataValue::Str(asset.asset_id.clone())),
(DataType::Size, DataValue::SignedNumber(asset.size.into())),
(DataType::Sha256, DataValue::Bytes(asset.sha256.clone())),
(DataType::MimeType, DataValue::Str(asset.mime_type.clone())),
(
DataType::VersionNumber,
DataValue::SignedNumber(asset.revision.into()),
),
(
DataType::CreatedAt,
DataValue::SignedNumber(asset.created_at.into()),
),
(
DataType::UpdatedAt,
DataValue::SignedNumber(asset.updated_at.into()),
),
])
}
fn asset_response(
cv: &CommunicationValue,
ty: CommunicationType,
asset: &UserAssetMetadata,
) -> CommunicationValue {
CommunicationValue::new(ty)
.with_request_id(cv)
.with_receiver(sender_wire_id(asset.user_id))
.add_typed_default(DataType::AssetId, DataValue::Str(asset.asset_id.clone()))
.add_typed_default(DataType::Size, DataValue::SignedNumber(asset.size.into()))
.add_typed_default(DataType::Sha256, DataValue::Bytes(asset.sha256.clone()))
.add_typed_default(DataType::MimeType, DataValue::Str(asset.mime_type.clone()))
.add_typed_default(
DataType::VersionNumber,
DataValue::SignedNumber(asset.revision.into()),
)
.add_typed_default(
DataType::CreatedAt,
DataValue::SignedNumber(asset.created_at.into()),
)
.add_typed_default(
DataType::UpdatedAt,
DataValue::SignedNumber(asset.updated_at.into()),
)
}
fn upload_response(
cv: &CommunicationValue,
ty: CommunicationType,
status: &UploadStatus,
) -> CommunicationValue {
let mut response = CommunicationValue::new(ty)
.with_request_id(cv)
.with_receiver(cv.sender().unwrap_or_default())
.add_typed_default(DataType::UploadId, DataValue::Str(status.upload_id.clone()))
.add_typed_default(DataType::AssetId, DataValue::Str(status.asset_id.clone()))
.add_typed_default(DataType::Size, DataValue::SignedNumber(status.size.into()))
.add_typed_default(DataType::Sha256, DataValue::Bytes(status.sha256.clone()))
.add_typed_default(DataType::MimeType, DataValue::Str(status.mime_type.clone()))
.add_typed_default(
DataType::Offset,
DataValue::SignedNumber(status.offset.into()),
)
.add_typed_default(DataType::State, DataValue::Str(status.state.clone()))
.add_typed_default(
DataType::CreatedAt,
DataValue::SignedNumber(status.created_at.into()),
)
.add_typed_default(
DataType::UpdatedAt,
DataValue::SignedNumber(status.updated_at.into()),
);
if let Some(revision) = status.revision {
response = response.add_typed_default(
DataType::VersionNumber,
DataValue::SignedNumber(revision.into()),
);
}
if let Some(committed_at) = status.committed_at {
response = response.add_typed_default(
DataType::CommittedAt,
DataValue::SignedNumber(committed_at.into()),
);
}
response
}
fn asset_error(cv: &CommunicationValue, error: StorageError) -> CommunicationValue {
error_response(cv, CommunicationType::ErrorInternal)
.add_typed_default(DataType::ErrorType, DataValue::Str(error.to_string()))
}
fn upload_request(cv: &CommunicationValue) -> Result<(i64, String), CommunicationValue> {
let user_id = required_sender_id(cv)?;
let upload_id = cv
.get_data(DataType::UploadId)
.as_str()
.filter(|id| !id.is_empty())
.ok_or_else(|| error_response(cv, CommunicationType::ErrorInvalidData))?;
Ok((user_id, upload_id.to_owned()))
}
fn setting_changed(setting: &SyncedSetting) -> CommunicationValue {
CommunicationValue::new(CommunicationType::SyncedSettingChanged)
.with_receiver(sender_wire_id(setting.user_id))
@ -2255,6 +2361,183 @@ pub fn handle_user_blob_list(cv: &CommunicationValue) -> CommunicationValue {
}
}
pub fn handle_user_asset_upload_start(cv: &CommunicationValue) -> CommunicationValue {
let user_id = match required_sender_id(cv) {
Ok(id) if id > 0 => id,
_ => return error_response(cv, CommunicationType::ErrorInvalidData),
};
let Some(upload_id) = cv.get_data(DataType::UploadId).as_str() else {
return error_response(cv, CommunicationType::ErrorInvalidData);
};
let Some(asset_id) = cv.get_data(DataType::AssetId).as_str() else {
return error_response(cv, CommunicationType::ErrorInvalidData);
};
let Some(size) = data_i64(cv, DataType::Size) else {
return error_response(cv, CommunicationType::ErrorInvalidData);
};
let Some(sha256) = cv.get_data(DataType::Sha256).and_then(DataValue::as_bytes) else {
return error_response(cv, CommunicationType::ErrorInvalidData);
};
let Some(mime_type) = cv.get_data(DataType::MimeType).as_str() else {
return error_response(cv, CommunicationType::ErrorInvalidData);
};
match user_assets::start(user_id, upload_id, asset_id, size, &sha256, mime_type) {
Ok(status) => upload_response(cv, CommunicationType::UserAssetUploadStart, &status),
Err(error) => asset_error(cv, error),
}
}
pub fn handle_user_asset_upload_chunk(cv: &CommunicationValue) -> CommunicationValue {
let (user_id, upload_id) = match upload_request(cv) {
Ok(value) => value,
Err(response) => return response,
};
let Some(offset) = data_i64(cv, DataType::Offset) else {
return error_response(cv, CommunicationType::ErrorInvalidData);
};
let Some(chunk) = cv.get_data(DataType::Blob).and_then(DataValue::as_bytes) else {
return error_response(cv, CommunicationType::ErrorInvalidData);
};
match user_assets::append(user_id, &upload_id, offset, &chunk) {
Ok(status) => upload_response(cv, CommunicationType::UserAssetUploadChunk, &status),
Err(error) => asset_error(cv, error),
}
}
pub fn handle_user_asset_upload_status(cv: &CommunicationValue) -> CommunicationValue {
let (user_id, upload_id) = match upload_request(cv) {
Ok(value) => value,
Err(response) => return response,
};
match user_assets::status(user_id, &upload_id) {
Ok(Some(status)) => upload_response(cv, CommunicationType::UserAssetUploadStatus, &status),
Ok(None) => error_response(cv, CommunicationType::ErrorNotFound),
Err(error) => asset_error(cv, error),
}
}
pub fn handle_user_asset_upload_commit(cv: &CommunicationValue) -> AssetMutation {
let (user_id, upload_id) = match upload_request(cv) {
Ok(value) => value,
Err(response) => {
return AssetMutation {
response,
changed: None,
};
}
};
match user_assets::commit(user_id, &upload_id) {
Ok((asset, changed)) => AssetMutation {
response: asset_response(cv, CommunicationType::UserAssetUploadCommit, &asset),
changed: changed.then(|| {
asset_response(cv, CommunicationType::UserAssetChanged, &asset)
.with_id(next_notification_id())
.add_typed_default(DataType::Deleted, DataValue::Bool(false))
}),
},
Err(error) => AssetMutation {
response: asset_error(cv, error),
changed: None,
},
}
}
pub fn handle_user_asset_upload_abort(cv: &CommunicationValue) -> CommunicationValue {
let (user_id, upload_id) = match upload_request(cv) {
Ok(value) => value,
Err(response) => return response,
};
match user_assets::abort(user_id, &upload_id) {
Ok(_) => CommunicationValue::new(CommunicationType::UserAssetUploadAbort)
.with_request_id(cv)
.with_receiver(sender_wire_id(user_id))
.add_typed_default(DataType::UploadId, DataValue::Str(upload_id)),
Err(error) => asset_error(cv, error),
}
}
pub fn handle_user_asset_get_chunk(cv: &CommunicationValue) -> CommunicationValue {
let user_id = match required_sender_id(cv) {
Ok(id) if id > 0 => id,
_ => return error_response(cv, CommunicationType::ErrorInvalidData),
};
let Some(asset_id) = cv.get_data(DataType::AssetId).as_str() else {
return error_response(cv, CommunicationType::ErrorInvalidData);
};
let Some(offset) = data_i64(cv, DataType::Offset) else {
return error_response(cv, CommunicationType::ErrorInvalidData);
};
let Some(amount) = data_i64(cv, DataType::Amount).and_then(|value| usize::try_from(value).ok())
else {
return error_response(cv, CommunicationType::ErrorInvalidData);
};
match user_assets::read_chunk(user_id, asset_id, offset, amount) {
Ok(Some((asset, bytes))) => {
asset_response(cv, CommunicationType::UserAssetGetChunk, &asset)
.add_typed_default(DataType::Offset, DataValue::SignedNumber(offset.into()))
.add_typed_default(DataType::Blob, DataValue::Bytes(bytes))
}
Ok(None) => error_response(cv, CommunicationType::ErrorNotFound),
Err(error) => asset_error(cv, error),
}
}
pub fn handle_user_asset_list(cv: &CommunicationValue) -> CommunicationValue {
let user_id = match required_sender_id(cv) {
Ok(id) if id > 0 => id,
_ => return error_response(cv, CommunicationType::ErrorInvalidData),
};
match user_assets::list(user_id) {
Ok(assets) => CommunicationValue::new(CommunicationType::UserAssetList)
.with_request_id(cv)
.with_receiver(sender_wire_id(user_id))
.add_typed_default(
DataType::Assets,
DataValue::Array(assets.iter().map(asset_metadata_value).collect()),
),
Err(error) => asset_error(cv, error),
}
}
pub fn handle_user_asset_delete(cv: &CommunicationValue) -> AssetMutation {
let user_id = match required_sender_id(cv) {
Ok(id) if id > 0 => id,
_ => {
return AssetMutation {
response: error_response(cv, CommunicationType::ErrorInvalidData),
changed: None,
};
}
};
let Some(asset_id) = cv.get_data(DataType::AssetId).as_str() else {
return AssetMutation {
response: error_response(cv, CommunicationType::ErrorInvalidData),
changed: None,
};
};
match user_assets::delete(user_id, asset_id) {
Ok(Some(asset)) => AssetMutation {
response: asset_response(cv, CommunicationType::UserAssetDelete, &asset),
changed: Some(
asset_response(cv, CommunicationType::UserAssetChanged, &asset)
.with_id(next_notification_id())
.add_typed_default(DataType::Deleted, DataValue::Bool(true)),
),
},
Ok(None) => AssetMutation {
response: CommunicationValue::new(CommunicationType::UserAssetDelete)
.with_request_id(cv)
.with_receiver(sender_wire_id(user_id))
.add_typed_default(DataType::AssetId, DataValue::Str(asset_id.to_owned())),
changed: None,
},
Err(error) => AssetMutation {
response: asset_error(cv, error),
changed: None,
},
}
}
fn authenticated_user(cv: &CommunicationValue) -> Result<i64, CommunicationValue> {
let user_id = required_sender_id(cv)?;
if user_id <= 0 {

View file

@ -66,6 +66,7 @@ mod security_tests {
CommunicationType::SyncedSettingDelete,
CommunicationType::SyncedSettingsList,
CommunicationType::SyncedSettingChanged,
CommunicationType::UserAssetChanged,
] {
assert_eq!(
message_security_class(&CommunicationValue::new(setting_type)),