From b1074e400e8a2e4cdc937ad4ae804c847731f953 Mon Sep 17 00:00:00 2001 From: Doug <6060466+pixlwave@users.noreply.github.com> Date: Wed, 15 Mar 2023 09:22:53 +0000 Subject: [PATCH] sdk: Add get_media_file function --- Cargo.lock | 1 + bindings/matrix-sdk-ffi/src/api.udl | 7 +++ bindings/matrix-sdk-ffi/src/client.rs | 38 ++++++++++++++- bindings/matrix-sdk-ffi/src/timeline.rs | 2 + crates/matrix-sdk/Cargo.toml | 3 +- crates/matrix-sdk/src/media.rs | 62 +++++++++++++++++++++++++ 6 files changed, 111 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 156603ba0..a067eab55 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2755,6 +2755,7 @@ dependencies = [ "matrix-sdk-sled", "matrix-sdk-test", "mime", + "mime_guess", "once_cell", "pin-project-lite", "rand 0.8.5", diff --git a/bindings/matrix-sdk-ffi/src/api.udl b/bindings/matrix-sdk-ffi/src/api.udl index 523f195ad..5b1254ec8 100644 --- a/bindings/matrix-sdk-ffi/src/api.udl +++ b/bindings/matrix-sdk-ffi/src/api.udl @@ -176,6 +176,13 @@ interface Client { [Throws=ClientError] void login(string username, string password, string? initial_device_name, string? device_id); + + [Throws=ClientError] + MediaFileHandle get_media_file(MediaSource source, string mime_type); +}; + +interface MediaFileHandle { + string path(); }; enum MembershipState { diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index cc4884ca1..ead71d48b 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -2,7 +2,7 @@ use std::sync::{Arc, RwLock}; use anyhow::{anyhow, Context}; use matrix_sdk::{ - media::{MediaFormat, MediaRequest, MediaThumbnailSize}, + media::{MediaFileHandle as SdkMediaFileHandle, MediaFormat, MediaRequest, MediaThumbnailSize}, ruma::{ api::client::{ account::whoami, @@ -181,6 +181,29 @@ impl Client { Ok(()) }) } + + pub fn get_media_file( + &self, + media_source: Arc, + mime_type: String, + ) -> anyhow::Result> { + let client = self.client.clone(); + let source = (*media_source).clone(); + let mime_type: mime::Mime = mime_type.parse()?; + + RUNTIME.block_on(async move { + let handle = client + .media() + .get_media_file( + &MediaRequest { source, format: MediaFormat::File }, + &mime_type, + true, + ) + .await?; + + Ok(Arc::new(MediaFileHandle { inner: handle })) + }) + } } #[uniffi::export] @@ -617,3 +640,16 @@ pub struct Session { fn gen_transaction_id() -> String { TransactionId::new().to_string() } + +/// A file handle that takes ownership of a media file on disk. When the handle +/// is dropped, the file will be removed from the disk. +pub struct MediaFileHandle { + inner: SdkMediaFileHandle, +} + +impl MediaFileHandle { + /// Get the media file's path. + pub fn path(&self) -> String { + self.inner.path().to_str().unwrap().to_owned() + } +} diff --git a/bindings/matrix-sdk-ffi/src/timeline.rs b/bindings/matrix-sdk-ffi/src/timeline.rs index d6a59700e..12f444f38 100644 --- a/bindings/matrix-sdk-ffi/src/timeline.rs +++ b/bindings/matrix-sdk-ffi/src/timeline.rs @@ -589,6 +589,7 @@ pub struct AudioInfo { // for that pub duration: Option, pub size: Option, + pub mimetype: Option, } #[derive(Clone, uniffi::Record)] @@ -681,6 +682,7 @@ impl From<&matrix_sdk::ruma::events::room::message::AudioInfo> for AudioInfo { Self { duration: info.duration.map(|d| d.as_millis() as u64), size: info.size.map(Into::into), + mimetype: info.mimetype.clone(), } } } diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index 89ae57e03..251909546 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -87,6 +87,7 @@ matrix-sdk-common = { version = "0.6.0", path = "../matrix-sdk-common" } matrix-sdk-indexeddb = { version = "0.2.0", path = "../matrix-sdk-indexeddb", default-features = false, optional = true } matrix-sdk-sled = { version = "0.2.0", path = "../matrix-sdk-sled", default-features = false, optional = true } mime = "0.3.16" +mime_guess = "2.0.4" pin-project-lite = "0.2.9" rand = { version = "0.8.5", optional = true } reqwest = { version = "0.11.10", default_features = false } @@ -94,6 +95,7 @@ ruma = { workspace = true, features = ["rand", "unstable-msc2448", "unstable-msc serde = { workspace = true } serde_html_form = { workspace = true } serde_json = { workspace = true } +tempfile = "3.3.0" thiserror = { workspace = true } tower = { version = "0.4.13", features = ["make"], optional = true } tracing = { workspace = true, features = ["attributes"] } @@ -137,7 +139,6 @@ dirs = "4.0.0" futures = { version = "0.3.21", default-features = false, features = ["executor"] } matrix-sdk-test = { version = "0.6.0", path = "../../testing/matrix-sdk-test" } once_cell = { workspace = true } -tempfile = "3.3.0" tracing-subscriber = { version = "0.3.11", features = ["env-filter"] } [target.'cfg(target_arch = "wasm32")'.dev-dependencies] diff --git a/crates/matrix-sdk/src/media.rs b/crates/matrix-sdk/src/media.rs index cd7e5a4e1..972f4b273 100644 --- a/crates/matrix-sdk/src/media.rs +++ b/crates/matrix-sdk/src/media.rs @@ -17,16 +17,24 @@ #[cfg(feature = "e2e-encryption")] use std::io::Read; +#[cfg(not(target_arch = "wasm32"))] +use std::path::Path; use std::time::Duration; pub use matrix_sdk_base::media::*; use mime::Mime; +#[cfg(not(target_arch = "wasm32"))] +use mime_guess; use ruma::{ api::client::media::{create_content, get_content, get_content_thumbnail}, assign, events::room::MediaSource, MxcUri, }; +#[cfg(not(target_arch = "wasm32"))] +use tempfile::{Builder as TempFileBuilder, NamedTempFile}; +#[cfg(not(target_arch = "wasm32"))] +use tokio::{fs::File as TokioFile, io::AsyncWriteExt}; use crate::{ attachment::{AttachmentInfo, Thumbnail}, @@ -45,6 +53,23 @@ pub struct Media { client: Client, } +/// A file handle that takes ownership of a media file on disk. When the handle +/// is dropped, the file will be removed from the disk. +#[derive(Debug)] +#[cfg(not(target_arch = "wasm32"))] +pub struct MediaFileHandle { + /// The temporary file that contains the media. + file: NamedTempFile, +} + +#[cfg(not(target_arch = "wasm32"))] +impl MediaFileHandle { + /// Get the media file's path. + pub fn path(&self) -> &Path { + self.file.path() + } +} + impl Media { pub(crate) fn new(client: Client) -> Self { Self { client } @@ -96,6 +121,43 @@ impl Media { Ok(self.client.send(request, Some(request_config)).await?) } + /// Gets a media file by copying it to a temporary location on disk. + /// + /// The file won't be encrypted even if it is encrypted on the server. + /// + /// Returns a `MediaFileHandle` which takes ownership of the file. When the + /// handle is dropped, the file will be deleted from the temporary location. + /// + /// # Arguments + /// + /// * `request` - The `MediaRequest` of the content. + /// + /// * `content_type` - The type of the media, this will be used to set the + /// temporary file's extension. + /// + /// * `use_cache` - If we should use the media cache for this request. + #[cfg(not(target_arch = "wasm32"))] + pub async fn get_media_file( + &self, + request: &MediaRequest, + content_type: &Mime, + use_cache: bool, + ) -> Result { + let data = self.get_media_content(request, use_cache).await?; + + let mut suffix = String::from(""); + if let Some(extension) = + mime_guess::get_mime_extensions(content_type).and_then(|a| a.first()) + { + suffix = String::from(".") + extension; + } + + let file = TempFileBuilder::new().suffix(&suffix).tempfile()?; + TokioFile::from_std(file.reopen()?).write_all(&data).await?; + + Ok(MediaFileHandle { file }) + } + /// Get a media file's content. /// /// If the content is encrypted and encryption is enabled, the content will