diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index c602deb9554..0a93944272b 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -39,8 +39,8 @@ use matrix_sdk::{ }, sliding_sync::Version as SdkSlidingSyncVersion, store::RoomLoadSettings as SdkRoomLoadSettings, - AuthApi, AuthSession, Client as MatrixClient, SessionChange, SessionTokens, - STATE_STORE_DATABASE_NAME, + AuthApi, AuthSession, Client as MatrixClient, SendMediaUploadRequest, SessionChange, + SessionTokens, STATE_STORE_DATABASE_NAME, }; use matrix_sdk_common::{stream::StreamExt, SendOutsideWasm, SyncOutsideWasm}; use matrix_sdk_ui::{ @@ -1026,13 +1026,16 @@ impl Client { &self, mime_type: String, data: Vec, + filename: Option, progress_watcher: Option>, ) -> Result { let mime_type: mime::Mime = mime_type.parse().context("Parsing mime type")?; - let request = self.inner.media().upload(&mime_type, data, None); + let send_media_request = SendMediaUploadRequest::new((*self.inner).clone(), data) + .with_content_type(mime_type.essence_str().to_owned()) + .with_filename(filename); if let Some(progress_watcher) = progress_watcher { - let mut subscriber = request.subscribe_to_send_progress(); + let mut subscriber = send_media_request.subscribe_to_send_progress(); get_runtime_handle().spawn(async move { while let Some(progress) = subscriber.next().await { progress_watcher.transmission_progress(progress.into()); @@ -1040,7 +1043,7 @@ impl Client { }); } - let response = request.await?; + let response = self.inner.media().upload(send_media_request).await?; Ok(String::from(response.content_uri)) } diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 641560f8999..79d3a82002b 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -219,7 +219,11 @@ pub enum UploadSource { /// Upload source is a file on disk File { /// Path to file - filename: String, + path: String, + + /// An optional filename, if the one in the path is not the one we want + /// to use for the file. + filename: Option, }, /// Upload source is data in memory Data { @@ -233,7 +237,7 @@ pub enum UploadSource { impl From for AttachmentSource { fn from(value: UploadSource) -> Self { match value { - UploadSource::File { filename } => Self::File(filename.into()), + UploadSource::File { path, filename } => Self::File { path: path.into(), filename }, UploadSource::Data { bytes, filename } => Self::Data { bytes, filename }, } } diff --git a/crates/matrix-sdk-base/src/media.rs b/crates/matrix-sdk-base/src/media.rs index 8a18e8258d2..8e1299fa5cf 100644 --- a/crates/matrix-sdk-base/src/media.rs +++ b/crates/matrix-sdk-base/src/media.rs @@ -142,6 +142,9 @@ pub trait MediaEventContent { /// Returns `None` if `Self` has no file. fn source(&self) -> Option; + /// Get the name of the uploaded file for `Self`. + fn filename_or_body(&self) -> Option; + /// Get the source of the thumbnail for `Self`. /// /// Returns `None` if `Self` has no thumbnail. @@ -153,6 +156,10 @@ impl MediaEventContent for StickerEventContent { Some(MediaSource::from(self.source.clone())) } + fn filename_or_body(&self) -> Option { + None + } + fn thumbnail_source(&self) -> Option { None } @@ -163,6 +170,14 @@ impl MediaEventContent for AudioMessageEventContent { Some(self.source.clone()) } + fn filename_or_body(&self) -> Option { + if let Some(filename) = &self.filename { + Some(filename.clone()) + } else { + Some(self.body.clone()) + } + } + fn thumbnail_source(&self) -> Option { None } @@ -173,6 +188,14 @@ impl MediaEventContent for FileMessageEventContent { Some(self.source.clone()) } + fn filename_or_body(&self) -> Option { + if let Some(filename) = &self.filename { + Some(filename.clone()) + } else { + Some(self.body.clone()) + } + } + fn thumbnail_source(&self) -> Option { self.info.as_ref()?.thumbnail_source.clone() } @@ -183,6 +206,14 @@ impl MediaEventContent for ImageMessageEventContent { Some(self.source.clone()) } + fn filename_or_body(&self) -> Option { + if let Some(filename) = &self.filename { + Some(filename.clone()) + } else { + Some(self.body.clone()) + } + } + fn thumbnail_source(&self) -> Option { self.info .as_ref() @@ -196,6 +227,14 @@ impl MediaEventContent for VideoMessageEventContent { Some(self.source.clone()) } + fn filename_or_body(&self) -> Option { + if let Some(filename) = &self.filename { + Some(filename.clone()) + } else { + Some(self.body.clone()) + } + } + fn thumbnail_source(&self) -> Option { self.info .as_ref() @@ -209,6 +248,10 @@ impl MediaEventContent for LocationMessageEventContent { None } + fn filename_or_body(&self) -> Option { + None + } + fn thumbnail_source(&self) -> Option { self.info.as_ref()?.thumbnail_source.clone() } diff --git a/crates/matrix-sdk-base/src/store/send_queue.rs b/crates/matrix-sdk-base/src/store/send_queue.rs index 877ad0b4b39..325db7c62b2 100644 --- a/crates/matrix-sdk-base/src/store/send_queue.rs +++ b/crates/matrix-sdk-base/src/store/send_queue.rs @@ -109,6 +109,9 @@ pub enum QueuedRequestKind { #[cfg(feature = "unstable-msc4274")] #[serde(default)] accumulated: Vec, + + /// The name of the file to upload, if known or public. + filename: Option, }, } @@ -241,6 +244,9 @@ pub enum DependentQueuedRequestKind { #[cfg(feature = "unstable-msc4274")] #[serde(default = "default_parent_is_thumbnail_upload")] parent_is_thumbnail_upload: bool, + + /// The name of the file to upload, if known. + filename: Option, }, /// Finish an upload by updating references to the media cache and sending diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index beb28f6a5c7..76b789e9254 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -821,7 +821,7 @@ pub enum AttachmentSource { /// An attachment loaded from a file. /// /// The bytes and the filename will be read from the file at the given path. - File(PathBuf), + File { filename: Option, path: PathBuf }, } impl AttachmentSource { @@ -829,13 +829,16 @@ impl AttachmentSource { pub(crate) fn try_into_bytes_and_filename(self) -> Result<(Vec, String), Error> { match self { Self::Data { bytes, filename } => Ok((bytes, filename)), - Self::File(path) => { - let filename = path - .file_name() - .ok_or(Error::InvalidAttachmentFileName)? - .to_str() - .ok_or(Error::InvalidAttachmentFileName)? - .to_owned(); + Self::File { path, filename } => { + let filename = if let Some(filename) = filename { + filename + } else { + path.file_name() + .ok_or(Error::InvalidAttachmentFileName)? + .to_str() + .ok_or(Error::InvalidAttachmentFileName)? + .to_owned() + }; let bytes = fs::read(&path).map_err(|_| Error::InvalidAttachmentData)?; Ok((bytes, filename)) } @@ -848,7 +851,7 @@ where P: Into, { fn from(value: P) -> Self { - Self::File(value.into()) + Self::File { path: value.into(), filename: None } } } diff --git a/crates/matrix-sdk/src/account.rs b/crates/matrix-sdk/src/account.rs index 8dcf15e71d6..68bd995d1a5 100644 --- a/crates/matrix-sdk/src/account.rs +++ b/crates/matrix-sdk/src/account.rs @@ -55,7 +55,7 @@ use ruma::{ use serde::Deserialize; use tracing::error; -use crate::{config::RequestConfig, Client, Error, Result}; +use crate::{config::RequestConfig, Client, Error, Result, SendMediaUploadRequest}; /// A high-level API to manage the client owner's account. /// @@ -264,7 +264,10 @@ impl Account { /// /// [`Media::upload()`]: crate::Media::upload pub async fn upload_avatar(&self, content_type: &Mime, data: Vec) -> Result { - let upload_response = self.client.media().upload(content_type, data, None).await?; + let send_media_request = SendMediaUploadRequest::new(self.client.clone(), data) + .with_content_type(content_type.essence_str().to_owned()); + + let upload_response = self.client.media().upload(send_media_request).await?; self.set_avatar_url(Some(&upload_response.content_uri)).await?; Ok(upload_response.content_uri) } diff --git a/crates/matrix-sdk/src/client/futures.rs b/crates/matrix-sdk/src/client/futures.rs index e83997b16f1..3953f115a97 100644 --- a/crates/matrix-sdk/src/client/futures.rs +++ b/crates/matrix-sdk/src/client/futures.rs @@ -156,17 +156,50 @@ where } } -/// `IntoFuture` used to send media upload requests. It wraps another -/// [`SendRequest`], checking its size will be accepted by the homeserver before -/// uploading. +/// `IntoFuture` used to send media upload requests. It works as a builder which +/// can create a [`SendRequest`] given several configuration options, checking +/// its size will be accepted by the homeserver before uploading. #[allow(missing_debug_implementations)] pub struct SendMediaUploadRequest { - send_request: SendRequest, + client: Client, + request: media::create_content::v3::Request, + /// The [`RequestConfig`] to use for uploading the media file. + pub request_config: Option, + progress_observable: SharedObservable, } impl SendMediaUploadRequest { - pub fn new(request: SendRequest) -> Self { - Self { send_request: request } + /// Creates a new instance. + pub fn new(client: Client, file: Vec) -> Self { + Self { + client, + request: media::create_content::v3::Request::new(file), + request_config: Default::default(), + progress_observable: SharedObservable::new(TransmissionProgress::default()), + } + } + + /// Returns the data to upload. + pub fn data(&self) -> &Vec { + &self.request.file + } + + /// Sets the content type of the media for the media upload request. + pub fn with_content_type(mut self, content_type: impl Into) -> Self { + self.request.content_type = Some(content_type.into()); + self + } + + /// Sets the filename for the media upload request. + pub fn with_filename(mut self, filename: Option>) -> Self { + self.request.filename = filename.map(Into::into); + self + } + + /// Applies the provided [`RequestConfig`] to the future [`SendRequest`]. + pub fn with_request_config(mut self, request_config: Option) -> Self { + self.request_config = request_config; + self } /// Replace the default `SharedObservable` used for tracking upload @@ -179,14 +212,24 @@ impl SendMediaUploadRequest { mut self, send_progress: SharedObservable, ) -> Self { - self.send_request = self.send_request.with_send_progress_observable(send_progress); + self.progress_observable = send_progress; self } /// Get a subscriber to observe the progress of sending the request /// body. pub fn subscribe_to_send_progress(&self) -> Subscriber { - self.send_request.send_progress.subscribe() + self.progress_observable.subscribe() + } + + /// Creates the [`SendRequest`] using the provided parameters. + pub fn build_send_request(self) -> SendRequest { + SendRequest { + client: self.client, + request: self.request, + config: self.request_config, + send_progress: self.progress_observable, + } } } @@ -195,9 +238,9 @@ impl IntoFuture for SendMediaUploadRequest { boxed_into_future!(); fn into_future(self) -> Self::IntoFuture { - let request_length = self.send_request.request.file.len(); - let client = self.send_request.client.clone(); - let send_request = self.send_request; + let send_request = self.build_send_request(); + let request_length = send_request.request.file.len(); + let client = send_request.client.clone(); Box::pin(async move { let max_upload_size = client.load_or_fetch_max_upload_size().await?; diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index cdf9a791700..5b204a4e169 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -111,7 +111,10 @@ mod builder; pub(crate) mod caches; pub(crate) mod futures; -pub use self::builder::{sanitize_server_name, ClientBuildError, ClientBuilder}; +pub use self::{ + builder::{sanitize_server_name, ClientBuildError, ClientBuilder}, + futures::SendMediaUploadRequest, +}; #[cfg(not(target_family = "wasm"))] type NotificationHandlerFut = Pin + Send>>; @@ -2909,7 +2912,6 @@ pub(crate) mod tests { use assert_matches::assert_matches; use assert_matches2::assert_let; - use eyeball::SharedObservable; use futures_util::{pin_mut, FutureExt}; use js_int::{uint, UInt}; use matrix_sdk_base::{ @@ -2950,10 +2952,9 @@ pub(crate) mod tests { use crate::{ client::{futures::SendMediaUploadRequest, WeakClient}, config::RequestConfig, - futures::SendRequest, media::MediaError, test_utils::{client::MockClientBuilder, mocks::MatrixMockServer}, - Error, TransmissionProgress, + Error, }; #[async_test] @@ -3781,15 +3782,7 @@ pub(crate) mod tests { assert_eq!(*client.inner.server_max_upload_size.lock().await.get().unwrap(), uint!(1)); let data = vec![1, 2]; - let upload_request = - ruma::api::client::media::create_content::v3::Request::new(data.clone()); - let request = SendRequest { - client: client.clone(), - request: upload_request, - config: None, - send_progress: SharedObservable::new(TransmissionProgress::default()), - }; - let media_request = SendMediaUploadRequest::new(request); + let media_request = SendMediaUploadRequest::new(client, data.clone()); let error = media_request.await.err(); assert_let!(Some(Error::Media(MediaError::MediaTooLargeToUpload { max, current })) = error); diff --git a/crates/matrix-sdk/src/encryption/futures.rs b/crates/matrix-sdk/src/encryption/futures.rs index a487ff8ed21..7a327bd609c 100644 --- a/crates/matrix-sdk/src/encryption/futures.rs +++ b/crates/matrix-sdk/src/encryption/futures.rs @@ -23,7 +23,9 @@ use eyeball::{SharedObservable, Subscriber}; use matrix_sdk_common::boxed_into_future; use ruma::events::room::{EncryptedFile, EncryptedFileInit}; -use crate::{config::RequestConfig, Client, Media, Result, TransmissionProgress}; +use crate::{ + config::RequestConfig, Client, Media, Result, SendMediaUploadRequest, TransmissionProgress, +}; /// Future returned by [`Client::upload_encrypted_file`]. #[allow(missing_debug_implementations)] @@ -89,11 +91,12 @@ where let request_config = request_config.map(|config| config.timeout(Media::reasonable_upload_timeout(&buf))); - let response = client - .media() - .upload(&mime::APPLICATION_OCTET_STREAM, buf, request_config) - .with_send_progress_observable(send_progress) - .await?; + let send_media_request = SendMediaUploadRequest::new(self.client.clone(), buf) + .with_content_type(mime::APPLICATION_OCTET_STREAM.essence_str()) + .with_request_config(request_config) + .with_send_progress_observable(send_progress); + + let response = client.media().upload(send_media_request).await?; let file: EncryptedFile = { let keys = encryptor.finish(); diff --git a/crates/matrix-sdk/src/lib.rs b/crates/matrix-sdk/src/lib.rs index 2680d4cb1e3..0e8e3a59abf 100644 --- a/crates/matrix-sdk/src/lib.rs +++ b/crates/matrix-sdk/src/lib.rs @@ -94,6 +94,9 @@ pub use sliding_sync::{ uniffi::setup_scaffolding!(); pub mod live_location_share; + +pub use client::SendMediaUploadRequest; + #[cfg(any(test, feature = "testing"))] pub mod test_utils; diff --git a/crates/matrix-sdk/src/media.rs b/crates/matrix-sdk/src/media.rs index b96df15622b..52d79c722a4 100644 --- a/crates/matrix-sdk/src/media.rs +++ b/crates/matrix-sdk/src/media.rs @@ -41,8 +41,8 @@ use tempfile::{Builder as TempFileBuilder, NamedTempFile, TempDir}; use tokio::{fs::File as TokioFile, io::AsyncWriteExt}; use crate::{ - attachment::Thumbnail, client::futures::SendMediaUploadRequest, config::RequestConfig, Client, - Error, Result, TransmissionProgress, + attachment::Thumbnail, client::futures::SendMediaUploadRequest, Client, Error, Result, + TransmissionProgress, }; /// A conservative upload speed of 1Mbps @@ -165,24 +165,14 @@ impl Media { Self { client } } - /// Upload some media to the server. - /// - /// # Arguments - /// - /// * `content_type` - The type of the media, this will be used as the - /// content-type header. - /// - /// * `data` - Vector of bytes to be uploaded to the server. - /// - /// * `request_config` - Optional request configuration for the HTTP client, - /// overriding the default. If not provided, a reasonable timeout value is - /// inferred. + /// Upload some media to the server using the provided + /// [`SendMediaUploadRequest`]. /// /// # Examples /// /// ```no_run /// # use std::fs; - /// # use matrix_sdk::{Client, ruma::room_id}; + /// # use matrix_sdk::{Client, ruma::room_id, SendMediaUploadRequest}; /// # use url::Url; /// # use mime; /// # async { @@ -190,28 +180,25 @@ impl Media { /// # let mut client = Client::new(homeserver).await?; /// let image = fs::read("/home/example/my-cat.jpg")?; /// - /// let response = - /// client.media().upload(&mime::IMAGE_JPEG, image, None).await?; + /// let send_media_request = SendMediaUploadRequest::new(client.clone(), image) + /// .with_content_type(mime::IMAGE_JPEG.essence_str().to_string()) + /// .with_filename(Some("my-cat.jpg")); + /// + /// let response = client.media().upload(send_media_request).await?; /// /// println!("Cat URI: {}", response.content_uri); /// # anyhow::Ok(()) }; /// ``` - pub fn upload( + pub async fn upload( &self, - content_type: &Mime, - data: Vec, - request_config: Option, - ) -> SendMediaUploadRequest { - let request_config = request_config.unwrap_or_else(|| { - self.client.request_config().timeout(Self::reasonable_upload_timeout(&data)) - }); - - let request = assign!(media::create_content::v3::Request::new(data), { - content_type: Some(content_type.essence_str().to_owned()), + send_media_upload_request: SendMediaUploadRequest, + ) -> Result { + let request_config = send_media_upload_request.request_config.unwrap_or_else(|| { + self.client + .request_config() + .timeout(Self::reasonable_upload_timeout(send_media_upload_request.data())) }); - - let request = self.client.send(request).with_request_config(request_config); - SendMediaUploadRequest::new(request) + send_media_upload_request.with_request_config(Some(request_config)).await } /// Returns a reasonable upload timeout for an upload, based on the size of @@ -721,14 +708,19 @@ impl Media { &self, content_type: &Mime, data: Vec, + filename: Option, thumbnail: Option, send_progress: SharedObservable, ) -> Result<(MediaSource, Option<(MediaSource, Box)>)> { - let upload_thumbnail = self.upload_thumbnail(thumbnail, send_progress.clone()); + let upload_thumbnail = + self.upload_thumbnail(thumbnail, filename.clone(), send_progress.clone()); - let upload_attachment = async move { - self.upload(content_type, data, None).with_send_progress_observable(send_progress).await - }; + let send_media_request = SendMediaUploadRequest::new(self.client.clone(), data) + .with_content_type(content_type.essence_str().to_owned()) + .with_filename(filename) + .with_send_progress_observable(send_progress); + + let upload_attachment = async move { self.upload(send_media_request).await }; let (thumbnail, response) = try_join(upload_thumbnail, upload_attachment).await?; @@ -740,6 +732,7 @@ impl Media { async fn upload_thumbnail( &self, thumbnail: Option, + filename: Option, send_progress: SharedObservable, ) -> Result)>> { let Some(thumbnail) = thumbnail else { @@ -748,10 +741,14 @@ impl Media { let (data, content_type, thumbnail_info) = thumbnail.into_parts(); - let response = self - .upload(&content_type, data, None) - .with_send_progress_observable(send_progress) - .await?; + let filename = filename.map(|name| format!("thumbnail-{name}")); + + let send_media_request = SendMediaUploadRequest::new(self.client.clone(), data) + .with_content_type(content_type.essence_str().to_owned()) + .with_filename(filename) + .with_send_progress_observable(send_progress); + + let response = self.upload(send_media_request).await?; let url = response.content_uri; Ok(Some((MediaSource::Plain(url), thumbnail_info))) diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index c61f18fcafd..322295edaa2 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -154,7 +154,8 @@ use crate::{ }, sync::RoomUpdate, utils::{IntoRawMessageLikeEventContent, IntoRawStateEventContent}, - BaseRoom, Client, Error, HttpResult, Result, RoomState, TransmissionProgress, + BaseRoom, Client, Error, HttpResult, Result, RoomState, SendMediaUploadRequest, + TransmissionProgress, }; #[cfg(feature = "e2e-encryption")] use crate::{crypto::types::events::CryptoContextInfo, encryption::backups::BackupState}; @@ -2247,6 +2248,7 @@ impl Room { // TODO: get rid of this clone; wait for Ruma to use `Bytes` or something // similar. data.clone(), + Some(filename.clone()), thumbnail, send_progress, ) @@ -2257,7 +2259,13 @@ impl Room { let (media_source, thumbnail) = self .client .media() - .upload_plain_media_and_thumbnail(content_type, data.clone(), thumbnail, send_progress) + .upload_plain_media_and_thumbnail( + content_type, + data.clone(), + Some(filename.clone()), + thumbnail, + send_progress, + ) .await?; if store_in_cache { @@ -2514,7 +2522,10 @@ impl Room { ) -> Result { self.ensure_room_joined()?; - let upload_response = self.client.media().upload(mime, data, None).await?; + let send_media_request = SendMediaUploadRequest::new(self.client.clone(), data) + .with_content_type(mime.essence_str().to_owned()); + + let upload_response = self.client.media().upload(send_media_request).await?; let mut info = info.unwrap_or_default(); info.blurhash = upload_response.blurhash; info.mimetype = Some(mime.to_string()); diff --git a/crates/matrix-sdk/src/send_queue/mod.rs b/crates/matrix-sdk/src/send_queue/mod.rs index c100fa1f80a..2460791819b 100644 --- a/crates/matrix-sdk/src/send_queue/mod.rs +++ b/crates/matrix-sdk/src/send_queue/mod.rs @@ -142,7 +142,7 @@ use as_variant::as_variant; use matrix_sdk_base::store::FinishGalleryItemInfo; use matrix_sdk_base::{ event_cache::store::EventCacheStoreError, - media::MediaRequestParameters, + media::{MediaEventContent, MediaRequestParameters}, store::{ ChildTransactionId, DependentQueuedRequest, DependentQueuedRequestKind, DynStateStore, FinishUploadThumbnailInfo, QueueWedgeError, QueuedRequest, QueuedRequestKind, @@ -158,7 +158,7 @@ use ruma::{ reaction::ReactionEventContent, relation::Annotation, room::{ - message::{FormattedBody, RoomMessageEventContent}, + message::{FormattedBody, MessageType, RoomMessageEventContent}, MediaSource, }, AnyMessageLikeEventContent, EventContent as _, Mentions, @@ -177,7 +177,7 @@ use crate::{ config::RequestConfig, error::RetryKind, room::{edit::EditedContent, WeakRoom}, - Client, Media, Room, + Client, Media, Room, SendMediaUploadRequest, }; mod upload; @@ -753,6 +753,7 @@ impl RoomSendQueue { related_to: relates_to, #[cfg(feature = "unstable-msc4274")] accumulated, + filename, } => { trace!(%relates_to, "uploading media related to event"); @@ -788,8 +789,13 @@ impl RoomSendQueue { trace!("upload will be in clear text (room without encryption)"); let request_config = RequestConfig::short_retry() .timeout(Media::reasonable_upload_timeout(&data)); - let res = - room.client().media().upload(&mime, data, Some(request_config)).await?; + + let send_media_request = SendMediaUploadRequest::new(room.client(), data) + .with_content_type(mime.essence_str()) + .with_request_config(Some(request_config)) + .with_filename(filename); + + let res = room.client().media().upload(send_media_request).await?; MediaSource::Plain(res.content_uri) }; @@ -797,8 +803,13 @@ impl RoomSendQueue { let media_source = { let request_config = RequestConfig::short_retry() .timeout(Media::reasonable_upload_timeout(&data)); - let res = - room.client().media().upload(&mime, data, Some(request_config)).await?; + + let send_media_request = SendMediaUploadRequest::new(room.client(), data) + .with_content_type(mime.essence_str()) + .with_request_config(Some(request_config)) + .with_filename(filename); + + let res = room.client().media().upload(send_media_request).await?; MediaSource::Plain(res.content_uri) }; @@ -1279,10 +1290,19 @@ impl QueueStorage { let client = guard.client()?; let store = client.state_store(); + let filename = match &event.msgtype { + MessageType::Image(msgtype) => msgtype.filename_or_body(), + MessageType::Audio(msgtype) => msgtype.filename_or_body(), + MessageType::Video(msgtype) => msgtype.filename_or_body(), + MessageType::File(msgtype) => msgtype.filename_or_body(), + _ => None, + }; + let thumbnail_info = self .push_thumbnail_and_media_uploads( store, &content_type, + filename, send_event_txn.clone(), created_at, upload_file_txn.clone(), @@ -1331,13 +1351,19 @@ impl QueueStorage { return Ok(()); }; - let GalleryItemQueueInfo { content_type, upload_file_txn, file_media_request, thumbnail } = - first; + let GalleryItemQueueInfo { + content_type, + upload_file_txn, + file_media_request, + filename, + thumbnail, + } = first; let thumbnail_info = self .push_thumbnail_and_media_uploads( store, content_type, + filename.clone(), send_event_txn.clone(), created_at, upload_file_txn.clone(), @@ -1356,6 +1382,7 @@ impl QueueStorage { content_type, upload_file_txn, file_media_request, + filename, thumbnail, } = item_queue_info; @@ -1378,6 +1405,7 @@ impl QueueStorage { cache_key: thumbnail_media_request.clone(), related_to: send_event_txn.clone(), parent_is_thumbnail_upload: false, + filename: filename.clone().map(|name| format!("thumbnail-{name}")), }, ) .await?; @@ -1401,6 +1429,7 @@ impl QueueStorage { cache_key: file_media_request.clone(), related_to: send_event_txn.clone(), parent_is_thumbnail_upload: thumbnail.is_some(), + filename: filename.clone(), }, ) .await?; @@ -1441,6 +1470,7 @@ impl QueueStorage { &self, store: &DynStateStore, content_type: &Mime, + filename: Option, send_event_txn: OwnedTransactionId, created_at: MilliSecondsSinceUnixEpoch, upload_file_txn: OwnedTransactionId, @@ -1463,6 +1493,7 @@ impl QueueStorage { related_to: send_event_txn.clone(), #[cfg(feature = "unstable-msc4274")] accumulated: vec![], + filename: filename.clone().map(|name| format!("thumbnail-{name}")), }, Self::LOW_PRIORITY, ) @@ -1481,6 +1512,7 @@ impl QueueStorage { related_to: send_event_txn, #[cfg(feature = "unstable-msc4274")] parent_is_thumbnail_upload: true, + filename, }, ) .await?; @@ -1500,6 +1532,7 @@ impl QueueStorage { related_to: send_event_txn, #[cfg(feature = "unstable-msc4274")] accumulated: vec![], + filename, }, Self::LOW_PRIORITY, ) @@ -1873,7 +1906,9 @@ impl QueueStorage { related_to, #[cfg(feature = "unstable-msc4274")] parent_is_thumbnail_upload, + filename, } => { + warn!("Saving send queue request for filename {filename:?}"); let Some(parent_key) = parent_key else { // Not finished yet, we should retry later => false. return Ok(false); @@ -1898,6 +1933,7 @@ impl QueueStorage { cache_key, related_to, parent_is_thumbnail_upload, + filename, ) .await?; } @@ -2039,6 +2075,7 @@ struct GalleryItemQueueInfo { content_type: Mime, upload_file_txn: OwnedTransactionId, file_media_request: MediaRequestParameters, + filename: Option, thumbnail: Option<(FinishUploadThumbnailInfo, MediaRequestParameters, Mime)>, } diff --git a/crates/matrix-sdk/src/send_queue/upload.rs b/crates/matrix-sdk/src/send_queue/upload.rs index 0ba73221db2..157bae3f1a5 100644 --- a/crates/matrix-sdk/src/send_queue/upload.rs +++ b/crates/matrix-sdk/src/send_queue/upload.rs @@ -344,7 +344,7 @@ impl RoomSendQueue { item_types.push(Room::make_gallery_item_type( &content_type, - filename, + filename.clone(), file_media_request.source.clone(), item_info.caption, item_info.formatted_caption, @@ -356,6 +356,7 @@ impl RoomSendQueue { content_type, upload_file_txn: upload_file_txn.clone(), file_media_request, + filename: Some(filename), thumbnail: queue_thumbnail_info, }); @@ -609,6 +610,7 @@ impl QueueStorage { cache_key: MediaRequestParameters, event_txn: OwnedTransactionId, parent_is_thumbnail_upload: bool, + filename: Option, ) -> Result<(), RoomSendQueueError> { // The previous file or thumbnail has been sent, now transform the dependent // file or thumbnail upload request into a ready one. @@ -657,6 +659,7 @@ impl QueueStorage { related_to: event_txn, #[cfg(feature = "unstable-msc4274")] accumulated, + filename, }; client diff --git a/crates/matrix-sdk/src/test_utils/mocks/mod.rs b/crates/matrix-sdk/src/test_utils/mocks/mod.rs index 80abc29968e..83f9c8b90ec 100644 --- a/crates/matrix-sdk/src/test_utils/mocks/mod.rs +++ b/crates/matrix-sdk/src/test_utils/mocks/mod.rs @@ -2583,7 +2583,8 @@ impl<'a> MockEndpoint<'a, UploadEndpoint> { /// # Examples /// /// ```no_run - /// # tokio_test::block_on(async { + /// # use matrix_sdk::SendMediaUploadRequest; + /// tokio_test::block_on(async { /// use matrix_sdk::{ /// ruma::{event_id, mxc_uri, room_id}, /// test_utils::mocks::MatrixMockServer, @@ -2595,7 +2596,10 @@ impl<'a> MockEndpoint<'a, UploadEndpoint> { /// let (receiver, upload_mock) = server.mock_upload().ok_with_capture(mxid); /// let client = server.client_builder().build().await; /// - /// client.media().upload(&mime::TEXT_PLAIN, vec![1, 2, 3, 4, 5], None).await?; + /// let send_media_request = SendMediaUploadRequest::new(client.clone(), vec![1, 2, 3, 4, 5]) + /// .with_content_type(mime::TEXT_PLAIN.essence_str()); + /// + /// client.media().upload(send_media_request).await?; /// /// let uploaded = receiver.await?; /// @@ -2624,6 +2628,13 @@ impl<'a> MockEndpoint<'a, UploadEndpoint> { /// Returns a upload endpoint that emulates success, i.e. the media has been /// uploaded to the media server and can be accessed using the given /// event has been sent with the given [`MxcUri`]. + /// Expect the filename query param to match the provided value. + pub fn expect_filename(self, filename: &str) -> Self { + Self { mock: self.mock.and(query_param("filename", filename)), ..self } + } + + /// Returns a redact endpoint that emulates success, i.e. the redaction + /// event has been sent with the given event id. pub fn ok(self, mxc_id: &MxcUri) -> MatrixMock<'a> { self.respond_with(ResponseTemplate::new(200).set_body_json(json!({ "content_uri": mxc_id diff --git a/crates/matrix-sdk/tests/integration/room/attachment/mod.rs b/crates/matrix-sdk/tests/integration/room/attachment/mod.rs index d8377442436..349de50998f 100644 --- a/crates/matrix-sdk/tests/integration/room/attachment/mod.rs +++ b/crates/matrix-sdk/tests/integration/room/attachment/mod.rs @@ -104,6 +104,8 @@ async fn test_room_attachment_send_info() { mock.mock_authenticated_media_config().ok_default().mount().await; + let filename = "image.jpg"; + let expected_event_id = event_id!("$h29iv0s8:example.com"); mock.mock_room_send() .body_matches_partial_json(json!({ @@ -120,6 +122,15 @@ async fn test_room_attachment_send_info() { mock.mock_upload() .expect_mime_type("image/jpeg") + .expect_filename(filename) + .ok(mxc_uri!("mxc://example.com/AQwafuaFswefuhsfAFAgsw")) + .mock_once() + .mount() + .await; + + mock.mock_upload() + .expect_mime_type("image/jpeg") + .expect_filename("thumbnail-image.jpg") .ok(mxc_uri!("mxc://example.com/AQwafuaFswefuhsfAFAgsw")) .mock_once() .mount() @@ -135,10 +146,17 @@ async fn test_room_attachment_send_info() { width: Some(uint!(800)), ..Default::default() })) - .caption(Some("image caption".to_owned())); + .caption(Some("image caption".to_owned())) + .thumbnail(Some(Thumbnail { + data: "A thumbnail".as_bytes().to_owned(), + content_type: mime::IMAGE_JPEG, + height: uint!(200), + width: uint!(200), + size: uint!(200), + })); let response = room - .send_attachment("image.jpg", &mime::IMAGE_JPEG, b"Hello world".to_vec(), config) + .send_attachment(filename, &mime::IMAGE_JPEG, b"Hello world".to_vec(), config) .await .unwrap(); diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index 61b0113399b..b6acb8ee45c 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -110,10 +110,18 @@ async fn queue_attachment_with_thumbnail(q: &RoomSendQueue) -> (SendHandle, &'st fn mock_jpeg_upload<'a>( mock: &'a MatrixMockServer, mxc: &MxcUri, + filename: Option, lock: Arc>, ) -> MatrixMock<'a> { let mxc = mxc.to_owned(); - mock.mock_upload().expect_mime_type("image/jpeg").respond_with(move |_req: &Request| { + let mut mocked_upload = mock.mock_upload().expect_mime_type("image/jpeg"); + mocked_upload = if let Some(filename) = filename { + mocked_upload.expect_filename(&filename) + } else { + mocked_upload + }; + + mocked_upload.respond_with(move |_req: &Request| { // Wait for the signal from the main task that we can process this query. let mock_lock = lock.clone(); std::thread::spawn(move || { @@ -1903,14 +1911,19 @@ async fn test_media_uploads() { let allow_upload_lock = Arc::new(Mutex::new(())); let block_upload = allow_upload_lock.lock().await; - mock_jpeg_upload(&mock, mxc_uri!("mxc://sdk.rs/thumbnail"), allow_upload_lock.clone()) - .mock_once() - .mount() - .await; - mock_jpeg_upload(&mock, mxc_uri!("mxc://sdk.rs/media"), allow_upload_lock.clone()) + mock_jpeg_upload(&mock, mxc_uri!("mxc://sdk.rs/thumbnail"), None, allow_upload_lock.clone()) .mock_once() .mount() .await; + mock_jpeg_upload( + &mock, + mxc_uri!("mxc://sdk.rs/media"), + Some(filename.to_owned()), + allow_upload_lock.clone(), + ) + .mock_once() + .mount() + .await; // ---------------------- // Send the media. @@ -2200,22 +2213,42 @@ async fn test_gallery_uploads() { let allow_upload_lock = Arc::new(Mutex::new(())); let block_upload = allow_upload_lock.lock().await; - mock_jpeg_upload(&mock, mxc_uri!("mxc://sdk.rs/thumbnail1"), allow_upload_lock.clone()) - .mock_once() - .mount() - .await; - mock_jpeg_upload(&mock, mxc_uri!("mxc://sdk.rs/media1"), allow_upload_lock.clone()) - .mock_once() - .mount() - .await; - mock_jpeg_upload(&mock, mxc_uri!("mxc://sdk.rs/thumbnail2"), allow_upload_lock.clone()) - .mock_once() - .mount() - .await; - mock_jpeg_upload(&mock, mxc_uri!("mxc://sdk.rs/media2"), allow_upload_lock.clone()) - .mock_once() - .mount() - .await; + mock_jpeg_upload( + &mock, + mxc_uri!("mxc://sdk.rs/thumbnail1"), + Some(format!("thumbnail-{filename1}")), + allow_upload_lock.clone(), + ) + .mock_once() + .mount() + .await; + mock_jpeg_upload( + &mock, + mxc_uri!("mxc://sdk.rs/media1"), + Some(filename1.to_owned()), + allow_upload_lock.clone(), + ) + .mock_once() + .mount() + .await; + mock_jpeg_upload( + &mock, + mxc_uri!("mxc://sdk.rs/thumbnail2"), + Some(format!("thumbnail-{filename2}")), + allow_upload_lock.clone(), + ) + .mock_once() + .mount() + .await; + mock_jpeg_upload( + &mock, + mxc_uri!("mxc://sdk.rs/media2"), + Some(filename2.to_owned()), + allow_upload_lock.clone(), + ) + .mock_once() + .mount() + .await; // ---------------------- // Send the media.