atagen

matrix-rust-sdk

public
Star0
Watch0
ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk.git

Blame: crates/matrix-sdk/src/media.rs

7ff8adafe9e4atagen2026-10-22 12:30 UTC1
// Copyright 2021 Kévin Commaille
7ff8adafe9e4atagen2026-10-22 12:30 UTC2
// Copyright 2022 The Matrix.org Foundation C.I.C.
7ff8adafe9e4atagen2026-10-22 12:30 UTC3
//
7ff8adafe9e4atagen2026-10-22 12:30 UTC4
// Licensed under the Apache License, Version 2.0 (the "License");
7ff8adafe9e4atagen2026-10-22 12:30 UTC5
// you may not use this file except in compliance with the License.
7ff8adafe9e4atagen2026-10-22 12:30 UTC6
// You may obtain a copy of the License at
7ff8adafe9e4atagen2026-10-22 12:30 UTC7
//
7ff8adafe9e4atagen2026-10-22 12:30 UTC8
//     http://www.apache.org/licenses/LICENSE-2.0
7ff8adafe9e4atagen2026-10-22 12:30 UTC9
//
7ff8adafe9e4atagen2026-10-22 12:30 UTC10
// Unless required by applicable law or agreed to in writing, software
7ff8adafe9e4atagen2026-10-22 12:30 UTC11
// distributed under the License is distributed on an "AS IS" BASIS,
7ff8adafe9e4atagen2026-10-22 12:30 UTC12
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7ff8adafe9e4atagen2026-10-22 12:30 UTC13
// See the License for the specific language governing permissions and
7ff8adafe9e4atagen2026-10-22 12:30 UTC14
// limitations under the License.
7ff8adafe9e4atagen2026-10-22 12:30 UTC15
7ff8adafe9e4atagen2026-10-22 12:30 UTC16
//! High-level media API.
7ff8adafe9e4atagen2026-10-22 12:30 UTC17
7ff8adafe9e4atagen2026-10-22 12:30 UTC18
#[cfg(feature = "e2e-encryption")]
7ff8adafe9e4atagen2026-10-22 12:30 UTC19
use std::io::Read;
7ff8adafe9e4atagen2026-10-22 12:30 UTC20
use std::time::Duration;
7ff8adafe9e4atagen2026-10-22 12:30 UTC21
#[cfg(not(target_family = "wasm"))]
7ff8adafe9e4atagen2026-10-22 12:30 UTC22
use std::{fmt, fs::File, path::Path};
7ff8adafe9e4atagen2026-10-22 12:30 UTC23
7ff8adafe9e4atagen2026-10-22 12:30 UTC24
use eyeball::SharedObservable;
7ff8adafe9e4atagen2026-10-22 12:30 UTC25
use futures_util::future::try_join;
7ff8adafe9e4atagen2026-10-22 12:30 UTC26
use matrix_sdk_base::media::store::IgnoreMediaRetentionPolicy;
7ff8adafe9e4atagen2026-10-22 12:30 UTC27
pub use matrix_sdk_base::media::{store::MediaRetentionPolicy, *};
7ff8adafe9e4atagen2026-10-22 12:30 UTC28
use mime::Mime;
7ff8adafe9e4atagen2026-10-22 12:30 UTC29
use ruma::{
7ff8adafe9e4atagen2026-10-22 12:30 UTC30
    MilliSecondsSinceUnixEpoch, MxcUri, OwnedMxcUri, TransactionId, UInt,
7ff8adafe9e4atagen2026-10-22 12:30 UTC31
    api::{
7ff8adafe9e4atagen2026-10-22 12:30 UTC32
        Metadata,
7ff8adafe9e4atagen2026-10-22 12:30 UTC33
        client::{authenticated_media, media},
7ff8adafe9e4atagen2026-10-22 12:30 UTC34
        error::ErrorKind,
7ff8adafe9e4atagen2026-10-22 12:30 UTC35
    },
7ff8adafe9e4atagen2026-10-22 12:30 UTC36
    assign,
7ff8adafe9e4atagen2026-10-22 12:30 UTC37
    events::room::{MediaSource, ThumbnailInfo},
7ff8adafe9e4atagen2026-10-22 12:30 UTC38
};
7ff8adafe9e4atagen2026-10-22 12:30 UTC39
#[cfg(not(target_family = "wasm"))]
7ff8adafe9e4atagen2026-10-22 12:30 UTC40
use tempfile::{Builder as TempFileBuilder, NamedTempFile, TempDir};
7ff8adafe9e4atagen2026-10-22 12:30 UTC41
#[cfg(not(target_family = "wasm"))]
7ff8adafe9e4atagen2026-10-22 12:30 UTC42
use tokio::{fs::File as TokioFile, io::AsyncWriteExt};
7ff8adafe9e4atagen2026-10-22 12:30 UTC43
7ff8adafe9e4atagen2026-10-22 12:30 UTC44
use crate::{
7ff8adafe9e4atagen2026-10-22 12:30 UTC45
    Client, Error, Result, TransmissionProgress, attachment::Thumbnail,
7ff8adafe9e4atagen2026-10-22 12:30 UTC46
    client::futures::SendMediaUploadRequest, config::RequestConfig,
7ff8adafe9e4atagen2026-10-22 12:30 UTC47
};
7ff8adafe9e4atagen2026-10-22 12:30 UTC48
7ff8adafe9e4atagen2026-10-22 12:30 UTC49
/// A conservative upload speed of 1Mbps
7ff8adafe9e4atagen2026-10-22 12:30 UTC50
const DEFAULT_UPLOAD_SPEED: u64 = 125_000;
7ff8adafe9e4atagen2026-10-22 12:30 UTC51
/// 5 min minimal upload request timeout, used to clamp the request timeout.
7ff8adafe9e4atagen2026-10-22 12:30 UTC52
const MIN_UPLOAD_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 5);
7ff8adafe9e4atagen2026-10-22 12:30 UTC53
/// The server name used to generate local MXC URIs.
7ff8adafe9e4atagen2026-10-22 12:30 UTC54
// This mustn't represent a potentially valid media server, otherwise it'd be
7ff8adafe9e4atagen2026-10-22 12:30 UTC55
// possible for an attacker to return malicious content under some
7ff8adafe9e4atagen2026-10-22 12:30 UTC56
// preconditions (e.g. the cache store has been cleared before the upload
7ff8adafe9e4atagen2026-10-22 12:30 UTC57
// took place). To mitigate against this, we use the .localhost TLD,
7ff8adafe9e4atagen2026-10-22 12:30 UTC58
// which is guaranteed to be on the local machine. As a result, the only attack
7ff8adafe9e4atagen2026-10-22 12:30 UTC59
// possible would be coming from the user themselves, which we consider a
7ff8adafe9e4atagen2026-10-22 12:30 UTC60
// non-threat.
7ff8adafe9e4atagen2026-10-22 12:30 UTC61
const LOCAL_MXC_SERVER_NAME: &str = "send-queue.localhost";
7ff8adafe9e4atagen2026-10-22 12:30 UTC62
7ff8adafe9e4atagen2026-10-22 12:30 UTC63
/// A high-level API to interact with the media API.
7ff8adafe9e4atagen2026-10-22 12:30 UTC64
#[derive(Debug, Clone)]
7ff8adafe9e4atagen2026-10-22 12:30 UTC65
pub struct Media {
7ff8adafe9e4atagen2026-10-22 12:30 UTC66
    /// The underlying HTTP client.
7ff8adafe9e4atagen2026-10-22 12:30 UTC67
    client: Client,
7ff8adafe9e4atagen2026-10-22 12:30 UTC68
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC69
7ff8adafe9e4atagen2026-10-22 12:30 UTC70
/// A file handle that takes ownership of a media file on disk. When the handle
7ff8adafe9e4atagen2026-10-22 12:30 UTC71
/// is dropped, the file will be removed from the disk.
7ff8adafe9e4atagen2026-10-22 12:30 UTC72
#[derive(Debug)]
7ff8adafe9e4atagen2026-10-22 12:30 UTC73
#[cfg(not(target_family = "wasm"))]
7ff8adafe9e4atagen2026-10-22 12:30 UTC74
pub struct MediaFileHandle {
7ff8adafe9e4atagen2026-10-22 12:30 UTC75
    /// The temporary file that contains the media.
7ff8adafe9e4atagen2026-10-22 12:30 UTC76
    file: NamedTempFile,
7ff8adafe9e4atagen2026-10-22 12:30 UTC77
    /// An intermediary temporary directory used in certain cases.
7ff8adafe9e4atagen2026-10-22 12:30 UTC78
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC79
    /// Only stored for its `Drop` semantics.
7ff8adafe9e4atagen2026-10-22 12:30 UTC80
    _directory: Option<TempDir>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC81
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC82
7ff8adafe9e4atagen2026-10-22 12:30 UTC83
#[cfg(not(target_family = "wasm"))]
7ff8adafe9e4atagen2026-10-22 12:30 UTC84
impl MediaFileHandle {
7ff8adafe9e4atagen2026-10-22 12:30 UTC85
    /// Get the media file's path.
7ff8adafe9e4atagen2026-10-22 12:30 UTC86
    pub fn path(&self) -> &Path {
7ff8adafe9e4atagen2026-10-22 12:30 UTC87
        self.file.path()
7ff8adafe9e4atagen2026-10-22 12:30 UTC88
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC89
7ff8adafe9e4atagen2026-10-22 12:30 UTC90
    /// Persist the media file to the given path.
7ff8adafe9e4atagen2026-10-22 12:30 UTC91
    pub fn persist(self, path: &Path) -> Result<File, PersistError> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC92
        self.file.persist(path).map_err(|e| PersistError {
7ff8adafe9e4atagen2026-10-22 12:30 UTC93
            error: e.error,
7ff8adafe9e4atagen2026-10-22 12:30 UTC94
            file: Self { file: e.file, _directory: self._directory },
7ff8adafe9e4atagen2026-10-22 12:30 UTC95
        })
7ff8adafe9e4atagen2026-10-22 12:30 UTC96
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC97
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC98
7ff8adafe9e4atagen2026-10-22 12:30 UTC99
/// Error returned when [`MediaFileHandle::persist`] fails.
7ff8adafe9e4atagen2026-10-22 12:30 UTC100
#[cfg(not(target_family = "wasm"))]
7ff8adafe9e4atagen2026-10-22 12:30 UTC101
pub struct PersistError {
7ff8adafe9e4atagen2026-10-22 12:30 UTC102
    /// The underlying IO error.
7ff8adafe9e4atagen2026-10-22 12:30 UTC103
    pub error: std::io::Error,
7ff8adafe9e4atagen2026-10-22 12:30 UTC104
    /// The temporary file that couldn't be persisted.
7ff8adafe9e4atagen2026-10-22 12:30 UTC105
    pub file: MediaFileHandle,
7ff8adafe9e4atagen2026-10-22 12:30 UTC106
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC107
7ff8adafe9e4atagen2026-10-22 12:30 UTC108
#[cfg(not(any(target_family = "wasm", tarpaulin_include)))]
7ff8adafe9e4atagen2026-10-22 12:30 UTC109
impl fmt::Debug for PersistError {
7ff8adafe9e4atagen2026-10-22 12:30 UTC110
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7ff8adafe9e4atagen2026-10-22 12:30 UTC111
        write!(f, "PersistError({:?})", self.error)
7ff8adafe9e4atagen2026-10-22 12:30 UTC112
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC113
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC114
7ff8adafe9e4atagen2026-10-22 12:30 UTC115
#[cfg(not(any(target_family = "wasm", tarpaulin_include)))]
7ff8adafe9e4atagen2026-10-22 12:30 UTC116
impl fmt::Display for PersistError {
7ff8adafe9e4atagen2026-10-22 12:30 UTC117
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7ff8adafe9e4atagen2026-10-22 12:30 UTC118
        write!(f, "failed to persist temporary file: {}", self.error)
7ff8adafe9e4atagen2026-10-22 12:30 UTC119
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC120
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC121
7ff8adafe9e4atagen2026-10-22 12:30 UTC122
/// A preallocated MXC URI created by [`Media::create_content_uri()`], and
7ff8adafe9e4atagen2026-10-22 12:30 UTC123
/// to be used with [`Media::upload_preallocated()`].
7ff8adafe9e4atagen2026-10-22 12:30 UTC124
#[derive(Debug)]
7ff8adafe9e4atagen2026-10-22 12:30 UTC125
pub struct PreallocatedMxcUri {
7ff8adafe9e4atagen2026-10-22 12:30 UTC126
    /// The URI for the media URI.
7ff8adafe9e4atagen2026-10-22 12:30 UTC127
    pub uri: OwnedMxcUri,
7ff8adafe9e4atagen2026-10-22 12:30 UTC128
    /// The expiration date for the media URI.
7ff8adafe9e4atagen2026-10-22 12:30 UTC129
    expire_date: Option<MilliSecondsSinceUnixEpoch>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC130
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC131
7ff8adafe9e4atagen2026-10-22 12:30 UTC132
/// An error that happened in the realm of media.
7ff8adafe9e4atagen2026-10-22 12:30 UTC133
#[derive(Debug, thiserror::Error)]
7ff8adafe9e4atagen2026-10-22 12:30 UTC134
pub enum MediaError {
7ff8adafe9e4atagen2026-10-22 12:30 UTC135
    /// A preallocated MXC URI has expired.
7ff8adafe9e4atagen2026-10-22 12:30 UTC136
    #[error("a preallocated MXC URI has expired")]
7ff8adafe9e4atagen2026-10-22 12:30 UTC137
    ExpiredPreallocatedMxcUri,
7ff8adafe9e4atagen2026-10-22 12:30 UTC138
7ff8adafe9e4atagen2026-10-22 12:30 UTC139
    /// Preallocated media already had content, cannot overwrite.
7ff8adafe9e4atagen2026-10-22 12:30 UTC140
    #[error("preallocated media already had content, cannot overwrite")]
7ff8adafe9e4atagen2026-10-22 12:30 UTC141
    CannotOverwriteMedia,
7ff8adafe9e4atagen2026-10-22 12:30 UTC142
7ff8adafe9e4atagen2026-10-22 12:30 UTC143
    /// Local-only media content was not found.
7ff8adafe9e4atagen2026-10-22 12:30 UTC144
    #[error("local-only media content was not found")]
7ff8adafe9e4atagen2026-10-22 12:30 UTC145
    LocalMediaNotFound,
7ff8adafe9e4atagen2026-10-22 12:30 UTC146
7ff8adafe9e4atagen2026-10-22 12:30 UTC147
    /// The provided media is too large to upload.
7ff8adafe9e4atagen2026-10-22 12:30 UTC148
    #[error(
7ff8adafe9e4atagen2026-10-22 12:30 UTC149
        "The provided media is too large to upload. \
7ff8adafe9e4atagen2026-10-22 12:30 UTC150
         Maximum upload length is {max} bytes, tried to upload {current} bytes"
7ff8adafe9e4atagen2026-10-22 12:30 UTC151
    )]
7ff8adafe9e4atagen2026-10-22 12:30 UTC152
    MediaTooLargeToUpload {
7ff8adafe9e4atagen2026-10-22 12:30 UTC153
        /// The `max_upload_size` value for this homeserver.
7ff8adafe9e4atagen2026-10-22 12:30 UTC154
        max: UInt,
7ff8adafe9e4atagen2026-10-22 12:30 UTC155
        /// The size of the current media to upload.
7ff8adafe9e4atagen2026-10-22 12:30 UTC156
        current: UInt,
7ff8adafe9e4atagen2026-10-22 12:30 UTC157
    },
7ff8adafe9e4atagen2026-10-22 12:30 UTC158
7ff8adafe9e4atagen2026-10-22 12:30 UTC159
    /// Fetching the `max_upload_size` value from the homeserver failed.
7ff8adafe9e4atagen2026-10-22 12:30 UTC160
    #[error("Fetching the `max_upload_size` value from the homeserver failed: {0}")]
7ff8adafe9e4atagen2026-10-22 12:30 UTC161
    FetchMaxUploadSizeFailed(String),
7ff8adafe9e4atagen2026-10-22 12:30 UTC162
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC163
7ff8adafe9e4atagen2026-10-22 12:30 UTC164
impl Media {
7ff8adafe9e4atagen2026-10-22 12:30 UTC165
    pub(crate) fn new(client: Client) -> Self {
7ff8adafe9e4atagen2026-10-22 12:30 UTC166
        Self { client }
7ff8adafe9e4atagen2026-10-22 12:30 UTC167
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC168
7ff8adafe9e4atagen2026-10-22 12:30 UTC169
    /// Upload some media to the server.
7ff8adafe9e4atagen2026-10-22 12:30 UTC170
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC171
    /// # Arguments
7ff8adafe9e4atagen2026-10-22 12:30 UTC172
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC173
    /// * `content_type` - The type of the media, this will be used as the
7ff8adafe9e4atagen2026-10-22 12:30 UTC174
    ///   content-type header.
7ff8adafe9e4atagen2026-10-22 12:30 UTC175
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC176
    /// * `data` - Vector of bytes to be uploaded to the server.
7ff8adafe9e4atagen2026-10-22 12:30 UTC177
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC178
    /// * `request_config` - Optional request configuration for the HTTP client,
7ff8adafe9e4atagen2026-10-22 12:30 UTC179
    ///   overriding the default. If not provided, a reasonable timeout value is
7ff8adafe9e4atagen2026-10-22 12:30 UTC180
    ///   inferred.
7ff8adafe9e4atagen2026-10-22 12:30 UTC181
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC182
    /// # Examples
7ff8adafe9e4atagen2026-10-22 12:30 UTC183
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC184
    /// ```no_run
7ff8adafe9e4atagen2026-10-22 12:30 UTC185
    /// # use std::fs;
7ff8adafe9e4atagen2026-10-22 12:30 UTC186
    /// # use matrix_sdk::{Client, ruma::room_id};
7ff8adafe9e4atagen2026-10-22 12:30 UTC187
    /// # use url::Url;
7ff8adafe9e4atagen2026-10-22 12:30 UTC188
    /// # use mime;
7ff8adafe9e4atagen2026-10-22 12:30 UTC189
    /// # async {
7ff8adafe9e4atagen2026-10-22 12:30 UTC190
    /// # let homeserver = Url::parse("http://localhost:8080")?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC191
    /// # let mut client = Client::new(homeserver).await?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC192
    /// let image = fs::read("/home/example/my-cat.jpg")?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC193
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC194
    /// let response =
7ff8adafe9e4atagen2026-10-22 12:30 UTC195
    ///     client.media().upload(&mime::IMAGE_JPEG, image, None).await?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC196
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC197
    /// println!("Cat URI: {}", response.content_uri);
7ff8adafe9e4atagen2026-10-22 12:30 UTC198
    /// # anyhow::Ok(()) };
7ff8adafe9e4atagen2026-10-22 12:30 UTC199
    /// ```
7ff8adafe9e4atagen2026-10-22 12:30 UTC200
    pub fn upload(
7ff8adafe9e4atagen2026-10-22 12:30 UTC201
        &self,
7ff8adafe9e4atagen2026-10-22 12:30 UTC202
        content_type: &Mime,
7ff8adafe9e4atagen2026-10-22 12:30 UTC203
        data: Vec<u8>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC204
        request_config: Option<RequestConfig>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC205
    ) -> SendMediaUploadRequest {
7ff8adafe9e4atagen2026-10-22 12:30 UTC206
        let request_config = request_config.unwrap_or_else(|| {
7ff8adafe9e4atagen2026-10-22 12:30 UTC207
            self.client.request_config().timeout(Self::reasonable_upload_timeout(&data))
7ff8adafe9e4atagen2026-10-22 12:30 UTC208
        });
7ff8adafe9e4atagen2026-10-22 12:30 UTC209
7ff8adafe9e4atagen2026-10-22 12:30 UTC210
        let request = assign!(media::create_content::v3::Request::new(data), {
7ff8adafe9e4atagen2026-10-22 12:30 UTC211
            content_type: Some(content_type.essence_str().to_owned()),
7ff8adafe9e4atagen2026-10-22 12:30 UTC212
        });
7ff8adafe9e4atagen2026-10-22 12:30 UTC213
7ff8adafe9e4atagen2026-10-22 12:30 UTC214
        let request = self.client.send(request).with_request_config(request_config);
7ff8adafe9e4atagen2026-10-22 12:30 UTC215
        SendMediaUploadRequest::new(request)
7ff8adafe9e4atagen2026-10-22 12:30 UTC216
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC217
7ff8adafe9e4atagen2026-10-22 12:30 UTC218
    /// Returns a reasonable upload timeout for an upload, based on the size of
7ff8adafe9e4atagen2026-10-22 12:30 UTC219
    /// the data to be uploaded.
7ff8adafe9e4atagen2026-10-22 12:30 UTC220
    pub(crate) fn reasonable_upload_timeout(data: &[u8]) -> Duration {
7ff8adafe9e4atagen2026-10-22 12:30 UTC221
        std::cmp::max(
7ff8adafe9e4atagen2026-10-22 12:30 UTC222
            Duration::from_secs(data.len() as u64 / DEFAULT_UPLOAD_SPEED),
7ff8adafe9e4atagen2026-10-22 12:30 UTC223
            MIN_UPLOAD_REQUEST_TIMEOUT,
7ff8adafe9e4atagen2026-10-22 12:30 UTC224
        )
7ff8adafe9e4atagen2026-10-22 12:30 UTC225
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC226
7ff8adafe9e4atagen2026-10-22 12:30 UTC227
    /// Preallocates an MXC URI for a media that will be uploaded soon.
7ff8adafe9e4atagen2026-10-22 12:30 UTC228
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC229
    /// This preallocates an URI *before* any content is uploaded to the server.
7ff8adafe9e4atagen2026-10-22 12:30 UTC230
    /// The resulting preallocated MXC URI can then be consumed with
7ff8adafe9e4atagen2026-10-22 12:30 UTC231
    /// [`Media::upload_preallocated`].
7ff8adafe9e4atagen2026-10-22 12:30 UTC232
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC233
    /// # Examples
7ff8adafe9e4atagen2026-10-22 12:30 UTC234
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC235
    /// ```no_run
7ff8adafe9e4atagen2026-10-22 12:30 UTC236
    /// # use std::fs;
7ff8adafe9e4atagen2026-10-22 12:30 UTC237
    /// # use matrix_sdk::{Client, ruma::room_id};
7ff8adafe9e4atagen2026-10-22 12:30 UTC238
    /// # use url::Url;
7ff8adafe9e4atagen2026-10-22 12:30 UTC239
    /// # use mime;
7ff8adafe9e4atagen2026-10-22 12:30 UTC240
    /// # async {
7ff8adafe9e4atagen2026-10-22 12:30 UTC241
    /// # let homeserver = Url::parse("http://localhost:8080")?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC242
    /// # let mut client = Client::new(homeserver).await?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC243
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC244
    /// let preallocated = client.media().create_content_uri().await?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC245
    /// println!("Cat URI: {}", preallocated.uri);
7ff8adafe9e4atagen2026-10-22 12:30 UTC246
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC247
    /// let image = fs::read("/home/example/my-cat.jpg")?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC248
    /// client
7ff8adafe9e4atagen2026-10-22 12:30 UTC249
    ///     .media()
7ff8adafe9e4atagen2026-10-22 12:30 UTC250
    ///     .upload_preallocated(preallocated, &mime::IMAGE_JPEG, image)
7ff8adafe9e4atagen2026-10-22 12:30 UTC251
    ///     .await?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC252
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC253
    /// # anyhow::Ok(()) };
7ff8adafe9e4atagen2026-10-22 12:30 UTC254
    /// ```
7ff8adafe9e4atagen2026-10-22 12:30 UTC255
    pub async fn create_content_uri(&self) -> Result<PreallocatedMxcUri> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC256
        // Note: this request doesn't have any parameters.
7ff8adafe9e4atagen2026-10-22 12:30 UTC257
        let request = media::create_mxc_uri::v1::Request::default();
7ff8adafe9e4atagen2026-10-22 12:30 UTC258
7ff8adafe9e4atagen2026-10-22 12:30 UTC259
        let response = self.client.send(request).await?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC260
7ff8adafe9e4atagen2026-10-22 12:30 UTC261
        Ok(PreallocatedMxcUri {
7ff8adafe9e4atagen2026-10-22 12:30 UTC262
            uri: response.content_uri,
7ff8adafe9e4atagen2026-10-22 12:30 UTC263
            expire_date: response.unused_expires_at,
7ff8adafe9e4atagen2026-10-22 12:30 UTC264
        })
7ff8adafe9e4atagen2026-10-22 12:30 UTC265
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC266
7ff8adafe9e4atagen2026-10-22 12:30 UTC267
    /// Fills the content of a preallocated MXC URI with the given content type
7ff8adafe9e4atagen2026-10-22 12:30 UTC268
    /// and data.
7ff8adafe9e4atagen2026-10-22 12:30 UTC269
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC270
    /// The URI must have been preallocated with [`Self::create_content_uri`].
7ff8adafe9e4atagen2026-10-22 12:30 UTC271
    /// See this method's documentation for a full example.
7ff8adafe9e4atagen2026-10-22 12:30 UTC272
    pub async fn upload_preallocated(
7ff8adafe9e4atagen2026-10-22 12:30 UTC273
        &self,
7ff8adafe9e4atagen2026-10-22 12:30 UTC274
        uri: PreallocatedMxcUri,
7ff8adafe9e4atagen2026-10-22 12:30 UTC275
        content_type: &Mime,
7ff8adafe9e4atagen2026-10-22 12:30 UTC276
        data: Vec<u8>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC277
    ) -> Result<()> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC278
        // Do a best-effort at reporting an expired MXC URI here; otherwise the server
7ff8adafe9e4atagen2026-10-22 12:30 UTC279
        // may complain about it later.
7ff8adafe9e4atagen2026-10-22 12:30 UTC280
        if let Some(expire_date) = uri.expire_date
7ff8adafe9e4atagen2026-10-22 12:30 UTC281
            && MilliSecondsSinceUnixEpoch::now() >= expire_date
7ff8adafe9e4atagen2026-10-22 12:30 UTC282
        {
7ff8adafe9e4atagen2026-10-22 12:30 UTC283
            return Err(Error::Media(MediaError::ExpiredPreallocatedMxcUri));
7ff8adafe9e4atagen2026-10-22 12:30 UTC284
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC285
7ff8adafe9e4atagen2026-10-22 12:30 UTC286
        let timeout = std::cmp::max(
7ff8adafe9e4atagen2026-10-22 12:30 UTC287
            Duration::from_secs(data.len() as u64 / DEFAULT_UPLOAD_SPEED),
7ff8adafe9e4atagen2026-10-22 12:30 UTC288
            MIN_UPLOAD_REQUEST_TIMEOUT,
7ff8adafe9e4atagen2026-10-22 12:30 UTC289
        );
7ff8adafe9e4atagen2026-10-22 12:30 UTC290
7ff8adafe9e4atagen2026-10-22 12:30 UTC291
        let request = assign!(media::create_content_async::v3::Request::from_url(&uri.uri, data)?, {
7ff8adafe9e4atagen2026-10-22 12:30 UTC292
            content_type: Some(content_type.as_ref().to_owned()),
7ff8adafe9e4atagen2026-10-22 12:30 UTC293
        });
7ff8adafe9e4atagen2026-10-22 12:30 UTC294
7ff8adafe9e4atagen2026-10-22 12:30 UTC295
        let request_config = self.client.request_config().timeout(timeout);
7ff8adafe9e4atagen2026-10-22 12:30 UTC296
7ff8adafe9e4atagen2026-10-22 12:30 UTC297
        if let Err(err) = self.client.send(request).with_request_config(request_config).await {
7ff8adafe9e4atagen2026-10-22 12:30 UTC298
            match err.client_api_error_kind() {
7ff8adafe9e4atagen2026-10-22 12:30 UTC299
                Some(ErrorKind::CannotOverwriteMedia) => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC300
                    Err(Error::Media(MediaError::CannotOverwriteMedia))
7ff8adafe9e4atagen2026-10-22 12:30 UTC301
                }
7ff8adafe9e4atagen2026-10-22 12:30 UTC302
7ff8adafe9e4atagen2026-10-22 12:30 UTC303
                // Unfortunately, the spec says a server will return 404 for either an expired MXC
7ff8adafe9e4atagen2026-10-22 12:30 UTC304
                // ID or a non-existing MXC ID. Do a best-effort guess to recognize an expired MXC
7ff8adafe9e4atagen2026-10-22 12:30 UTC305
                // ID based on the error string, which will work with Synapse (as of 2024-10-23).
7ff8adafe9e4atagen2026-10-22 12:30 UTC306
                Some(ErrorKind::Unknown) if err.to_string().contains("expired") => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC307
                    Err(Error::Media(MediaError::ExpiredPreallocatedMxcUri))
7ff8adafe9e4atagen2026-10-22 12:30 UTC308
                }
7ff8adafe9e4atagen2026-10-22 12:30 UTC309
7ff8adafe9e4atagen2026-10-22 12:30 UTC310
                _ => Err(err.into()),
7ff8adafe9e4atagen2026-10-22 12:30 UTC311
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC312
        } else {
7ff8adafe9e4atagen2026-10-22 12:30 UTC313
            Ok(())
7ff8adafe9e4atagen2026-10-22 12:30 UTC314
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC315
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC316
7ff8adafe9e4atagen2026-10-22 12:30 UTC317
    /// Gets a media file by copying it to a temporary location on disk.
7ff8adafe9e4atagen2026-10-22 12:30 UTC318
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC319
    /// The file won't be encrypted even if it is encrypted on the server.
7ff8adafe9e4atagen2026-10-22 12:30 UTC320
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC321
    /// Returns a `MediaFileHandle` which takes ownership of the file. When the
7ff8adafe9e4atagen2026-10-22 12:30 UTC322
    /// handle is dropped, the file will be deleted from the temporary location.
7ff8adafe9e4atagen2026-10-22 12:30 UTC323
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC324
    /// # Arguments
7ff8adafe9e4atagen2026-10-22 12:30 UTC325
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC326
    /// * `request` - The `MediaRequest` of the content.
7ff8adafe9e4atagen2026-10-22 12:30 UTC327
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC328
    /// * `filename` - The filename specified in the event. It is suggested to
7ff8adafe9e4atagen2026-10-22 12:30 UTC329
    ///   use the `filename()` method on the event's content instead of using
7ff8adafe9e4atagen2026-10-22 12:30 UTC330
    ///   the `filename` field directly. If not provided, a random name will be
7ff8adafe9e4atagen2026-10-22 12:30 UTC331
    ///   generated.
7ff8adafe9e4atagen2026-10-22 12:30 UTC332
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC333
    /// * `content_type` - The type of the media, this will be used to set the
7ff8adafe9e4atagen2026-10-22 12:30 UTC334
    ///   temporary file's extension when one isn't included in the filename.
7ff8adafe9e4atagen2026-10-22 12:30 UTC335
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC336
    /// * `use_cache` - If we should use the media cache for this request.
7ff8adafe9e4atagen2026-10-22 12:30 UTC337
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC338
    /// * `temp_dir` - Path to a directory where temporary directories can be
7ff8adafe9e4atagen2026-10-22 12:30 UTC339
    ///   created. If not provided, a default, global temporary directory will
7ff8adafe9e4atagen2026-10-22 12:30 UTC340
    ///   be used; this may not work properly on Android, where the default
7ff8adafe9e4atagen2026-10-22 12:30 UTC341
    ///   location may require root access on some older Android versions.
7ff8adafe9e4atagen2026-10-22 12:30 UTC342
    #[cfg(not(target_family = "wasm"))]
7ff8adafe9e4atagen2026-10-22 12:30 UTC343
    pub async fn get_media_file(
7ff8adafe9e4atagen2026-10-22 12:30 UTC344
        &self,
7ff8adafe9e4atagen2026-10-22 12:30 UTC345
        request: &MediaRequestParameters,
7ff8adafe9e4atagen2026-10-22 12:30 UTC346
        filename: Option<String>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC347
        content_type: &Mime,
7ff8adafe9e4atagen2026-10-22 12:30 UTC348
        use_cache: bool,
7ff8adafe9e4atagen2026-10-22 12:30 UTC349
        temp_dir: Option<String>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC350
    ) -> Result<MediaFileHandle> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC351
        let data = self.get_media_content(request, use_cache).await?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC352
7ff8adafe9e4atagen2026-10-22 12:30 UTC353
        let inferred_extension = mime2ext::mime2ext(content_type);
7ff8adafe9e4atagen2026-10-22 12:30 UTC354
7ff8adafe9e4atagen2026-10-22 12:30 UTC355
        let filename_as_path = filename.as_ref().map(Path::new);
7ff8adafe9e4atagen2026-10-22 12:30 UTC356
7ff8adafe9e4atagen2026-10-22 12:30 UTC357
        let (sanitized_filename, filename_has_extension) = if let Some(path) = filename_as_path {
7ff8adafe9e4atagen2026-10-22 12:30 UTC358
            let sanitized_filename = path.file_name().and_then(|f| f.to_str());
7ff8adafe9e4atagen2026-10-22 12:30 UTC359
            let filename_has_extension = path.extension().is_some();
7ff8adafe9e4atagen2026-10-22 12:30 UTC360
            (sanitized_filename, filename_has_extension)
7ff8adafe9e4atagen2026-10-22 12:30 UTC361
        } else {
7ff8adafe9e4atagen2026-10-22 12:30 UTC362
            (None, false)
7ff8adafe9e4atagen2026-10-22 12:30 UTC363
        };
7ff8adafe9e4atagen2026-10-22 12:30 UTC364
7ff8adafe9e4atagen2026-10-22 12:30 UTC365
        let (temp_file, temp_dir) =
7ff8adafe9e4atagen2026-10-22 12:30 UTC366
            match (sanitized_filename, filename_has_extension, inferred_extension) {
7ff8adafe9e4atagen2026-10-22 12:30 UTC367
                // If the file name has an extension use that
7ff8adafe9e4atagen2026-10-22 12:30 UTC368
                (Some(filename_with_extension), true, _) => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC369
                    // Use an intermediary directory to avoid conflicts
7ff8adafe9e4atagen2026-10-22 12:30 UTC370
                    let temp_dir = temp_dir.map(TempDir::new_in).unwrap_or_else(TempDir::new)?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC371
                    let temp_file = TempFileBuilder::new()
7ff8adafe9e4atagen2026-10-22 12:30 UTC372
                        .prefix(filename_with_extension)
7ff8adafe9e4atagen2026-10-22 12:30 UTC373
                        .rand_bytes(0)
7ff8adafe9e4atagen2026-10-22 12:30 UTC374
                        .tempfile_in(&temp_dir)?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC375
                    (temp_file, Some(temp_dir))
7ff8adafe9e4atagen2026-10-22 12:30 UTC376
                }
7ff8adafe9e4atagen2026-10-22 12:30 UTC377
                // If the file name doesn't have an extension try inferring one for it
7ff8adafe9e4atagen2026-10-22 12:30 UTC378
                (Some(filename), false, Some(inferred_extension)) => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC379
                    // Use an intermediary directory to avoid conflicts
7ff8adafe9e4atagen2026-10-22 12:30 UTC380
                    let temp_dir = temp_dir.map(TempDir::new_in).unwrap_or_else(TempDir::new)?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC381
                    let temp_file = TempFileBuilder::new()
7ff8adafe9e4atagen2026-10-22 12:30 UTC382
                        .prefix(filename)
7ff8adafe9e4atagen2026-10-22 12:30 UTC383
                        .suffix(&(".".to_owned() + inferred_extension))
7ff8adafe9e4atagen2026-10-22 12:30 UTC384
                        .rand_bytes(0)
7ff8adafe9e4atagen2026-10-22 12:30 UTC385
                        .tempfile_in(&temp_dir)?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC386
                    (temp_file, Some(temp_dir))
7ff8adafe9e4atagen2026-10-22 12:30 UTC387
                }
7ff8adafe9e4atagen2026-10-22 12:30 UTC388
                // If the only thing we have is an inferred extension then use that together with a
7ff8adafe9e4atagen2026-10-22 12:30 UTC389
                // randomly generated file name
7ff8adafe9e4atagen2026-10-22 12:30 UTC390
                (None, _, Some(inferred_extension)) => (
7ff8adafe9e4atagen2026-10-22 12:30 UTC391
                    TempFileBuilder::new()
7ff8adafe9e4atagen2026-10-22 12:30 UTC392
                        .suffix(&&(".".to_owned() + inferred_extension))
7ff8adafe9e4atagen2026-10-22 12:30 UTC393
                        .tempfile()?,
7ff8adafe9e4atagen2026-10-22 12:30 UTC394
                    None,
7ff8adafe9e4atagen2026-10-22 12:30 UTC395
                ),
7ff8adafe9e4atagen2026-10-22 12:30 UTC396
                // Otherwise just use a completely random file name
7ff8adafe9e4atagen2026-10-22 12:30 UTC397
                _ => (TempFileBuilder::new().tempfile()?, None),
7ff8adafe9e4atagen2026-10-22 12:30 UTC398
            };
7ff8adafe9e4atagen2026-10-22 12:30 UTC399
7ff8adafe9e4atagen2026-10-22 12:30 UTC400
        let mut file = TokioFile::from_std(temp_file.reopen()?);
7ff8adafe9e4atagen2026-10-22 12:30 UTC401
        file.write_all(&data).await?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC402
        // Make sure the file metadata is flushed to disk.
7ff8adafe9e4atagen2026-10-22 12:30 UTC403
        file.sync_all().await?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC404
7ff8adafe9e4atagen2026-10-22 12:30 UTC405
        Ok(MediaFileHandle { file: temp_file, _directory: temp_dir })
7ff8adafe9e4atagen2026-10-22 12:30 UTC406
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC407
7ff8adafe9e4atagen2026-10-22 12:30 UTC408
    /// Get a media file's content.
7ff8adafe9e4atagen2026-10-22 12:30 UTC409
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC410
    /// If the content is encrypted and encryption is enabled, the content will
7ff8adafe9e4atagen2026-10-22 12:30 UTC411
    /// be decrypted.
7ff8adafe9e4atagen2026-10-22 12:30 UTC412
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC413
    /// # Arguments
7ff8adafe9e4atagen2026-10-22 12:30 UTC414
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC415
    /// * `request` - The `MediaRequest` of the content.
7ff8adafe9e4atagen2026-10-22 12:30 UTC416
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC417
    /// * `use_cache` - If we should use the media cache for this request.
7ff8adafe9e4atagen2026-10-22 12:30 UTC418
    pub async fn get_media_content(
7ff8adafe9e4atagen2026-10-22 12:30 UTC419
        &self,
7ff8adafe9e4atagen2026-10-22 12:30 UTC420
        request: &MediaRequestParameters,
7ff8adafe9e4atagen2026-10-22 12:30 UTC421
        use_cache: bool,
7ff8adafe9e4atagen2026-10-22 12:30 UTC422
    ) -> Result<Vec<u8>> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC423
        // Ignore request parameters for local medias, notably those pending in the send
7ff8adafe9e4atagen2026-10-22 12:30 UTC424
        // queue.
7ff8adafe9e4atagen2026-10-22 12:30 UTC425
        if let Some(uri) = Self::as_local_uri(&request.source) {
7ff8adafe9e4atagen2026-10-22 12:30 UTC426
            return self.get_local_media_content(uri).await;
7ff8adafe9e4atagen2026-10-22 12:30 UTC427
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC428
7ff8adafe9e4atagen2026-10-22 12:30 UTC429
        // Read from the cache.
7ff8adafe9e4atagen2026-10-22 12:30 UTC430
        if use_cache
7ff8adafe9e4atagen2026-10-22 12:30 UTC431
            && let Some(content) =
7ff8adafe9e4atagen2026-10-22 12:30 UTC432
                self.client.media_store().lock().await?.get_media_content(request).await?
7ff8adafe9e4atagen2026-10-22 12:30 UTC433
        {
7ff8adafe9e4atagen2026-10-22 12:30 UTC434
            return Ok(content);
7ff8adafe9e4atagen2026-10-22 12:30 UTC435
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC436
7ff8adafe9e4atagen2026-10-22 12:30 UTC437
        let request_config = self
7ff8adafe9e4atagen2026-10-22 12:30 UTC438
            .client
7ff8adafe9e4atagen2026-10-22 12:30 UTC439
            .request_config()
7ff8adafe9e4atagen2026-10-22 12:30 UTC440
            // Downloading a file should have no timeout as we don't know the network connectivity
7ff8adafe9e4atagen2026-10-22 12:30 UTC441
            // available for the user or the file size
7ff8adafe9e4atagen2026-10-22 12:30 UTC442
            .timeout(Some(Duration::MAX));
7ff8adafe9e4atagen2026-10-22 12:30 UTC443
7ff8adafe9e4atagen2026-10-22 12:30 UTC444
        // Use the authenticated endpoints when the server supports it.
7ff8adafe9e4atagen2026-10-22 12:30 UTC445
        let supported_versions = self.client.supported_versions().await?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC446
7ff8adafe9e4atagen2026-10-22 12:30 UTC447
        let use_auth = authenticated_media::get_content::v1::Request::PATH_BUILDER
7ff8adafe9e4atagen2026-10-22 12:30 UTC448
            .is_supported(&supported_versions);
7ff8adafe9e4atagen2026-10-22 12:30 UTC449
7ff8adafe9e4atagen2026-10-22 12:30 UTC450
        let content: Vec<u8> = match &request.source {
7ff8adafe9e4atagen2026-10-22 12:30 UTC451
            MediaSource::Encrypted(file) => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC452
                let content = if use_auth {
7ff8adafe9e4atagen2026-10-22 12:30 UTC453
                    let request =
7ff8adafe9e4atagen2026-10-22 12:30 UTC454
                        authenticated_media::get_content::v1::Request::from_uri(&file.url)?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC455
                    self.client.send(request).with_request_config(request_config).await?.file
7ff8adafe9e4atagen2026-10-22 12:30 UTC456
                } else {
7ff8adafe9e4atagen2026-10-22 12:30 UTC457
                    #[allow(deprecated)]
7ff8adafe9e4atagen2026-10-22 12:30 UTC458
                    let request = media::get_content::v3::Request::from_url(&file.url)?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC459
                    self.client.send(request).with_request_config(request_config).await?.file
7ff8adafe9e4atagen2026-10-22 12:30 UTC460
                };
7ff8adafe9e4atagen2026-10-22 12:30 UTC461
7ff8adafe9e4atagen2026-10-22 12:30 UTC462
                #[cfg(feature = "e2e-encryption")]
7ff8adafe9e4atagen2026-10-22 12:30 UTC463
                let content = {
7ff8adafe9e4atagen2026-10-22 12:30 UTC464
                    let content_len = content.len();
7ff8adafe9e4atagen2026-10-22 12:30 UTC465
                    let mut cursor = std::io::Cursor::new(content);
7ff8adafe9e4atagen2026-10-22 12:30 UTC466
                    let mut reader = matrix_sdk_base::crypto::AttachmentDecryptor::new(
7ff8adafe9e4atagen2026-10-22 12:30 UTC467
                        &mut cursor,
7ff8adafe9e4atagen2026-10-22 12:30 UTC468
                        file.as_ref().clone().into(),
7ff8adafe9e4atagen2026-10-22 12:30 UTC469
                    )?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC470
7ff8adafe9e4atagen2026-10-22 12:30 UTC471
                    // Encrypted size should be the same as the decrypted size,
7ff8adafe9e4atagen2026-10-22 12:30 UTC472
                    // rounded up to a cipher block.
7ff8adafe9e4atagen2026-10-22 12:30 UTC473
                    let mut decrypted = Vec::with_capacity(content_len);
7ff8adafe9e4atagen2026-10-22 12:30 UTC474
7ff8adafe9e4atagen2026-10-22 12:30 UTC475
                    reader.read_to_end(&mut decrypted)?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC476
7ff8adafe9e4atagen2026-10-22 12:30 UTC477
                    decrypted
7ff8adafe9e4atagen2026-10-22 12:30 UTC478
                };
7ff8adafe9e4atagen2026-10-22 12:30 UTC479
7ff8adafe9e4atagen2026-10-22 12:30 UTC480
                content
7ff8adafe9e4atagen2026-10-22 12:30 UTC481
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC482
7ff8adafe9e4atagen2026-10-22 12:30 UTC483
            MediaSource::Plain(uri) => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC484
                if let MediaFormat::Thumbnail(settings) = &request.format {
7ff8adafe9e4atagen2026-10-22 12:30 UTC485
                    if use_auth {
7ff8adafe9e4atagen2026-10-22 12:30 UTC486
                        let mut request =
7ff8adafe9e4atagen2026-10-22 12:30 UTC487
                            authenticated_media::get_content_thumbnail::v1::Request::from_uri(
7ff8adafe9e4atagen2026-10-22 12:30 UTC488
                                uri,
7ff8adafe9e4atagen2026-10-22 12:30 UTC489
                                settings.width,
7ff8adafe9e4atagen2026-10-22 12:30 UTC490
                                settings.height,
7ff8adafe9e4atagen2026-10-22 12:30 UTC491
                            )?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC492
                        request.method = Some(settings.method.clone());
7ff8adafe9e4atagen2026-10-22 12:30 UTC493
                        request.animated = Some(settings.animated);
7ff8adafe9e4atagen2026-10-22 12:30 UTC494
7ff8adafe9e4atagen2026-10-22 12:30 UTC495
                        self.client.send(request).with_request_config(request_config).await?.file
7ff8adafe9e4atagen2026-10-22 12:30 UTC496
                    } else {
7ff8adafe9e4atagen2026-10-22 12:30 UTC497
                        #[allow(deprecated)]
7ff8adafe9e4atagen2026-10-22 12:30 UTC498
                        let request = {
7ff8adafe9e4atagen2026-10-22 12:30 UTC499
                            let mut request = media::get_content_thumbnail::v3::Request::from_url(
7ff8adafe9e4atagen2026-10-22 12:30 UTC500
                                uri,
7ff8adafe9e4atagen2026-10-22 12:30 UTC501
                                settings.width,
7ff8adafe9e4atagen2026-10-22 12:30 UTC502
                                settings.height,
7ff8adafe9e4atagen2026-10-22 12:30 UTC503
                            )?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC504
                            request.method = Some(settings.method.clone());
7ff8adafe9e4atagen2026-10-22 12:30 UTC505
                            request.animated = Some(settings.animated);
7ff8adafe9e4atagen2026-10-22 12:30 UTC506
                            request
7ff8adafe9e4atagen2026-10-22 12:30 UTC507
                        };
7ff8adafe9e4atagen2026-10-22 12:30 UTC508
7ff8adafe9e4atagen2026-10-22 12:30 UTC509
                        self.client.send(request).with_request_config(request_config).await?.file
7ff8adafe9e4atagen2026-10-22 12:30 UTC510
                    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC511
                } else if use_auth {
7ff8adafe9e4atagen2026-10-22 12:30 UTC512
                    let request = authenticated_media::get_content::v1::Request::from_uri(uri)?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC513
                    self.client.send(request).with_request_config(request_config).await?.file
7ff8adafe9e4atagen2026-10-22 12:30 UTC514
                } else {
7ff8adafe9e4atagen2026-10-22 12:30 UTC515
                    #[allow(deprecated)]
7ff8adafe9e4atagen2026-10-22 12:30 UTC516
                    let request = media::get_content::v3::Request::from_url(uri)?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC517
                    self.client.send(request).with_request_config(request_config).await?.file
7ff8adafe9e4atagen2026-10-22 12:30 UTC518
                }
7ff8adafe9e4atagen2026-10-22 12:30 UTC519
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC520
        };
7ff8adafe9e4atagen2026-10-22 12:30 UTC521
7ff8adafe9e4atagen2026-10-22 12:30 UTC522
        if use_cache {
7ff8adafe9e4atagen2026-10-22 12:30 UTC523
            self.client
7ff8adafe9e4atagen2026-10-22 12:30 UTC524
                .media_store()
7ff8adafe9e4atagen2026-10-22 12:30 UTC525
                .lock()
7ff8adafe9e4atagen2026-10-22 12:30 UTC526
                .await?
7ff8adafe9e4atagen2026-10-22 12:30 UTC527
                .add_media_content(request, content.clone(), IgnoreMediaRetentionPolicy::No)
7ff8adafe9e4atagen2026-10-22 12:30 UTC528
                .await?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC529
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC530
7ff8adafe9e4atagen2026-10-22 12:30 UTC531
        Ok(content)
7ff8adafe9e4atagen2026-10-22 12:30 UTC532
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC533
7ff8adafe9e4atagen2026-10-22 12:30 UTC534
    /// Get a media file's content that is only available in the media cache.
7ff8adafe9e4atagen2026-10-22 12:30 UTC535
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC536
    /// # Arguments
7ff8adafe9e4atagen2026-10-22 12:30 UTC537
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC538
    /// * `uri` - The local MXC URI of the media content.
7ff8adafe9e4atagen2026-10-22 12:30 UTC539
    async fn get_local_media_content(&self, uri: &MxcUri) -> Result<Vec<u8>> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC540
        // Read from the cache.
7ff8adafe9e4atagen2026-10-22 12:30 UTC541
        self.client
7ff8adafe9e4atagen2026-10-22 12:30 UTC542
            .media_store()
7ff8adafe9e4atagen2026-10-22 12:30 UTC543
            .lock()
7ff8adafe9e4atagen2026-10-22 12:30 UTC544
            .await?
7ff8adafe9e4atagen2026-10-22 12:30 UTC545
            .get_media_content_for_uri(uri)
7ff8adafe9e4atagen2026-10-22 12:30 UTC546
            .await?
7ff8adafe9e4atagen2026-10-22 12:30 UTC547
            .ok_or_else(|| MediaError::LocalMediaNotFound.into())
7ff8adafe9e4atagen2026-10-22 12:30 UTC548
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC549
7ff8adafe9e4atagen2026-10-22 12:30 UTC550
    /// Remove a media file's content from the store.
7ff8adafe9e4atagen2026-10-22 12:30 UTC551
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC552
    /// # Arguments
7ff8adafe9e4atagen2026-10-22 12:30 UTC553
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC554
    /// * `request` - The `MediaRequest` of the content.
7ff8adafe9e4atagen2026-10-22 12:30 UTC555
    pub async fn remove_media_content(&self, request: &MediaRequestParameters) -> Result<()> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC556
        Ok(self.client.media_store().lock().await?.remove_media_content(request).await?)
7ff8adafe9e4atagen2026-10-22 12:30 UTC557
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC558
7ff8adafe9e4atagen2026-10-22 12:30 UTC559
    /// Delete all the media content corresponding to the given
7ff8adafe9e4atagen2026-10-22 12:30 UTC560
    /// uri from the store.
7ff8adafe9e4atagen2026-10-22 12:30 UTC561
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC562
    /// # Arguments
7ff8adafe9e4atagen2026-10-22 12:30 UTC563
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC564
    /// * `uri` - The `MxcUri` of the files.
7ff8adafe9e4atagen2026-10-22 12:30 UTC565
    pub async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC566
        Ok(self.client.media_store().lock().await?.remove_media_content_for_uri(uri).await?)
7ff8adafe9e4atagen2026-10-22 12:30 UTC567
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC568
7ff8adafe9e4atagen2026-10-22 12:30 UTC569
    /// Get the file of the given media event content.
7ff8adafe9e4atagen2026-10-22 12:30 UTC570
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC571
    /// If the content is encrypted and encryption is enabled, the content will
7ff8adafe9e4atagen2026-10-22 12:30 UTC572
    /// be decrypted.
7ff8adafe9e4atagen2026-10-22 12:30 UTC573
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC574
    /// Returns `Ok(None)` if the event content has no file.
7ff8adafe9e4atagen2026-10-22 12:30 UTC575
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC576
    /// This is a convenience method that calls the
7ff8adafe9e4atagen2026-10-22 12:30 UTC577
    /// [`get_media_content`](#method.get_media_content) method.
7ff8adafe9e4atagen2026-10-22 12:30 UTC578
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC579
    /// # Arguments
7ff8adafe9e4atagen2026-10-22 12:30 UTC580
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC581
    /// * `event_content` - The media event content.
7ff8adafe9e4atagen2026-10-22 12:30 UTC582
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC583
    /// * `use_cache` - If we should use the media cache for this file.
7ff8adafe9e4atagen2026-10-22 12:30 UTC584
    pub async fn get_file(
7ff8adafe9e4atagen2026-10-22 12:30 UTC585
        &self,
7ff8adafe9e4atagen2026-10-22 12:30 UTC586
        event_content: &impl MediaEventContent,
7ff8adafe9e4atagen2026-10-22 12:30 UTC587
        use_cache: bool,
7ff8adafe9e4atagen2026-10-22 12:30 UTC588
    ) -> Result<Option<Vec<u8>>> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC589
        let Some(source) = event_content.source() else { return Ok(None) };
7ff8adafe9e4atagen2026-10-22 12:30 UTC590
        let file = self
7ff8adafe9e4atagen2026-10-22 12:30 UTC591
            .get_media_content(
7ff8adafe9e4atagen2026-10-22 12:30 UTC592
                &MediaRequestParameters { source, format: MediaFormat::File },
7ff8adafe9e4atagen2026-10-22 12:30 UTC593
                use_cache,
7ff8adafe9e4atagen2026-10-22 12:30 UTC594
            )
7ff8adafe9e4atagen2026-10-22 12:30 UTC595
            .await?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC596
        Ok(Some(file))
7ff8adafe9e4atagen2026-10-22 12:30 UTC597
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC598
7ff8adafe9e4atagen2026-10-22 12:30 UTC599
    /// Remove the file of the given media event content from the cache.
7ff8adafe9e4atagen2026-10-22 12:30 UTC600
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC601
    /// This is a convenience method that calls the
7ff8adafe9e4atagen2026-10-22 12:30 UTC602
    /// [`remove_media_content`](#method.remove_media_content) method.
7ff8adafe9e4atagen2026-10-22 12:30 UTC603
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC604
    /// # Arguments
7ff8adafe9e4atagen2026-10-22 12:30 UTC605
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC606
    /// * `event_content` - The media event content.
7ff8adafe9e4atagen2026-10-22 12:30 UTC607
    pub async fn remove_file(&self, event_content: &impl MediaEventContent) -> Result<()> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC608
        if let Some(source) = event_content.source() {
7ff8adafe9e4atagen2026-10-22 12:30 UTC609
            self.remove_media_content(&MediaRequestParameters {
7ff8adafe9e4atagen2026-10-22 12:30 UTC610
                source,
7ff8adafe9e4atagen2026-10-22 12:30 UTC611
                format: MediaFormat::File,
7ff8adafe9e4atagen2026-10-22 12:30 UTC612
            })
7ff8adafe9e4atagen2026-10-22 12:30 UTC613
            .await?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC614
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC615
7ff8adafe9e4atagen2026-10-22 12:30 UTC616
        Ok(())
7ff8adafe9e4atagen2026-10-22 12:30 UTC617
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC618
7ff8adafe9e4atagen2026-10-22 12:30 UTC619
    /// Get a thumbnail of the given media event content.
7ff8adafe9e4atagen2026-10-22 12:30 UTC620
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC621
    /// If the content is encrypted and encryption is enabled, the content will
7ff8adafe9e4atagen2026-10-22 12:30 UTC622
    /// be decrypted.
7ff8adafe9e4atagen2026-10-22 12:30 UTC623
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC624
    /// Returns `Ok(None)` if the event content has no thumbnail.
7ff8adafe9e4atagen2026-10-22 12:30 UTC625
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC626
    /// This is a convenience method that calls the
7ff8adafe9e4atagen2026-10-22 12:30 UTC627
    /// [`get_media_content`](#method.get_media_content) method.
7ff8adafe9e4atagen2026-10-22 12:30 UTC628
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC629
    /// # Arguments
7ff8adafe9e4atagen2026-10-22 12:30 UTC630
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC631
    /// * `event_content` - The media event content.
7ff8adafe9e4atagen2026-10-22 12:30 UTC632
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC633
    /// * `settings` - The _desired_ settings of the thumbnail. The actual
7ff8adafe9e4atagen2026-10-22 12:30 UTC634
    ///   thumbnail may not match the settings specified.
7ff8adafe9e4atagen2026-10-22 12:30 UTC635
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC636
    /// * `use_cache` - If we should use the media cache for this thumbnail.
7ff8adafe9e4atagen2026-10-22 12:30 UTC637
    pub async fn get_thumbnail(
7ff8adafe9e4atagen2026-10-22 12:30 UTC638
        &self,
7ff8adafe9e4atagen2026-10-22 12:30 UTC639
        event_content: &impl MediaEventContent,
7ff8adafe9e4atagen2026-10-22 12:30 UTC640
        settings: MediaThumbnailSettings,
7ff8adafe9e4atagen2026-10-22 12:30 UTC641
        use_cache: bool,
7ff8adafe9e4atagen2026-10-22 12:30 UTC642
    ) -> Result<Option<Vec<u8>>> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC643
        let Some(source) = event_content.thumbnail_source() else { return Ok(None) };
7ff8adafe9e4atagen2026-10-22 12:30 UTC644
        let thumbnail = self
7ff8adafe9e4atagen2026-10-22 12:30 UTC645
            .get_media_content(
7ff8adafe9e4atagen2026-10-22 12:30 UTC646
                &MediaRequestParameters { source, format: MediaFormat::Thumbnail(settings) },
7ff8adafe9e4atagen2026-10-22 12:30 UTC647
                use_cache,
7ff8adafe9e4atagen2026-10-22 12:30 UTC648
            )
7ff8adafe9e4atagen2026-10-22 12:30 UTC649
            .await?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC650
        Ok(Some(thumbnail))
7ff8adafe9e4atagen2026-10-22 12:30 UTC651
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC652
7ff8adafe9e4atagen2026-10-22 12:30 UTC653
    /// Remove the thumbnail of the given media event content from the cache.
7ff8adafe9e4atagen2026-10-22 12:30 UTC654
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC655
    /// This is a convenience method that calls the
7ff8adafe9e4atagen2026-10-22 12:30 UTC656
    /// [`remove_media_content`](#method.remove_media_content) method.
7ff8adafe9e4atagen2026-10-22 12:30 UTC657
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC658
    /// # Arguments
7ff8adafe9e4atagen2026-10-22 12:30 UTC659
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC660
    /// * `event_content` - The media event content.
7ff8adafe9e4atagen2026-10-22 12:30 UTC661
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC662
    /// * `size` - The _desired_ settings of the thumbnail. Must match the
7ff8adafe9e4atagen2026-10-22 12:30 UTC663
    ///   settings requested with [`get_thumbnail`](#method.get_thumbnail).
7ff8adafe9e4atagen2026-10-22 12:30 UTC664
    pub async fn remove_thumbnail(
7ff8adafe9e4atagen2026-10-22 12:30 UTC665
        &self,
7ff8adafe9e4atagen2026-10-22 12:30 UTC666
        event_content: &impl MediaEventContent,
7ff8adafe9e4atagen2026-10-22 12:30 UTC667
        settings: MediaThumbnailSettings,
7ff8adafe9e4atagen2026-10-22 12:30 UTC668
    ) -> Result<()> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC669
        if let Some(source) = event_content.source() {
7ff8adafe9e4atagen2026-10-22 12:30 UTC670
            self.remove_media_content(&MediaRequestParameters {
7ff8adafe9e4atagen2026-10-22 12:30 UTC671
                source,
7ff8adafe9e4atagen2026-10-22 12:30 UTC672
                format: MediaFormat::Thumbnail(settings),
7ff8adafe9e4atagen2026-10-22 12:30 UTC673
            })
7ff8adafe9e4atagen2026-10-22 12:30 UTC674
            .await?
7ff8adafe9e4atagen2026-10-22 12:30 UTC675
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC676
7ff8adafe9e4atagen2026-10-22 12:30 UTC677
        Ok(())
7ff8adafe9e4atagen2026-10-22 12:30 UTC678
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC679
7ff8adafe9e4atagen2026-10-22 12:30 UTC680
    /// Set the [`MediaRetentionPolicy`] to use for deciding whether to store or
7ff8adafe9e4atagen2026-10-22 12:30 UTC681
    /// keep media content.
7ff8adafe9e4atagen2026-10-22 12:30 UTC682
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC683
    /// It is used:
7ff8adafe9e4atagen2026-10-22 12:30 UTC684
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC685
    /// * When a media needs to be cached, to check that it does not exceed the
7ff8adafe9e4atagen2026-10-22 12:30 UTC686
    ///   max file size.
7ff8adafe9e4atagen2026-10-22 12:30 UTC687
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC688
    /// * When [`Media::clean()`], to check that all media content in the store
7ff8adafe9e4atagen2026-10-22 12:30 UTC689
    ///   fits those criteria.
7ff8adafe9e4atagen2026-10-22 12:30 UTC690
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC691
    /// To apply the new policy to the media cache right away,
7ff8adafe9e4atagen2026-10-22 12:30 UTC692
    /// [`Media::clean()`] should be called after this.
7ff8adafe9e4atagen2026-10-22 12:30 UTC693
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC694
    /// By default, an empty `MediaRetentionPolicy` is used, which means that no
7ff8adafe9e4atagen2026-10-22 12:30 UTC695
    /// criteria are applied.
7ff8adafe9e4atagen2026-10-22 12:30 UTC696
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC697
    /// # Arguments
7ff8adafe9e4atagen2026-10-22 12:30 UTC698
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC699
    /// * `policy` - The `MediaRetentionPolicy` to use.
7ff8adafe9e4atagen2026-10-22 12:30 UTC700
    pub async fn set_media_retention_policy(&self, policy: MediaRetentionPolicy) -> Result<()> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC701
        self.client.media_store().lock().await?.set_media_retention_policy(policy).await?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC702
        Ok(())
7ff8adafe9e4atagen2026-10-22 12:30 UTC703
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC704
7ff8adafe9e4atagen2026-10-22 12:30 UTC705
    /// Get the current `MediaRetentionPolicy`.
7ff8adafe9e4atagen2026-10-22 12:30 UTC706
    pub async fn media_retention_policy(&self) -> Result<MediaRetentionPolicy> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC707
        Ok(self.client.media_store().lock().await?.media_retention_policy())
7ff8adafe9e4atagen2026-10-22 12:30 UTC708
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC709
7ff8adafe9e4atagen2026-10-22 12:30 UTC710
    /// Clean up the media cache with the current [`MediaRetentionPolicy`].
7ff8adafe9e4atagen2026-10-22 12:30 UTC711
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC712
    /// If there is already an ongoing cleanup, this is a noop.
7ff8adafe9e4atagen2026-10-22 12:30 UTC713
    pub async fn clean(&self) -> Result<()> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC714
        self.client.media_store().lock().await?.clean().await?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC715
        Ok(())
7ff8adafe9e4atagen2026-10-22 12:30 UTC716
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC717
7ff8adafe9e4atagen2026-10-22 12:30 UTC718
    /// Upload the file bytes in `data` and return the source information.
7ff8adafe9e4atagen2026-10-22 12:30 UTC719
    pub(crate) async fn upload_plain_media_and_thumbnail(
7ff8adafe9e4atagen2026-10-22 12:30 UTC720
        &self,
7ff8adafe9e4atagen2026-10-22 12:30 UTC721
        content_type: &Mime,
7ff8adafe9e4atagen2026-10-22 12:30 UTC722
        data: Vec<u8>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC723
        thumbnail: Option<Thumbnail>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC724
        send_progress: SharedObservable<TransmissionProgress>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC725
    ) -> Result<(MediaSource, Option<(MediaSource, Box<ThumbnailInfo>)>)> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC726
        let upload_thumbnail = self.upload_thumbnail(thumbnail, send_progress.clone());
7ff8adafe9e4atagen2026-10-22 12:30 UTC727
7ff8adafe9e4atagen2026-10-22 12:30 UTC728
        let upload_attachment = async move {
7ff8adafe9e4atagen2026-10-22 12:30 UTC729
            self.upload(content_type, data, None).with_send_progress_observable(send_progress).await
7ff8adafe9e4atagen2026-10-22 12:30 UTC730
        };
7ff8adafe9e4atagen2026-10-22 12:30 UTC731
7ff8adafe9e4atagen2026-10-22 12:30 UTC732
        let (thumbnail, response) = try_join(upload_thumbnail, upload_attachment).await?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC733
7ff8adafe9e4atagen2026-10-22 12:30 UTC734
        Ok((MediaSource::Plain(response.content_uri), thumbnail))
7ff8adafe9e4atagen2026-10-22 12:30 UTC735
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC736
7ff8adafe9e4atagen2026-10-22 12:30 UTC737
    /// Uploads an unencrypted thumbnail to the media repository, and returns
7ff8adafe9e4atagen2026-10-22 12:30 UTC738
    /// its source and extra information.
7ff8adafe9e4atagen2026-10-22 12:30 UTC739
    async fn upload_thumbnail(
7ff8adafe9e4atagen2026-10-22 12:30 UTC740
        &self,
7ff8adafe9e4atagen2026-10-22 12:30 UTC741
        thumbnail: Option<Thumbnail>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC742
        send_progress: SharedObservable<TransmissionProgress>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC743
    ) -> Result<Option<(MediaSource, Box<ThumbnailInfo>)>> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC744
        let Some(thumbnail) = thumbnail else {
7ff8adafe9e4atagen2026-10-22 12:30 UTC745
            return Ok(None);
7ff8adafe9e4atagen2026-10-22 12:30 UTC746
        };
7ff8adafe9e4atagen2026-10-22 12:30 UTC747
7ff8adafe9e4atagen2026-10-22 12:30 UTC748
        let (data, content_type, thumbnail_info) = thumbnail.into_parts();
7ff8adafe9e4atagen2026-10-22 12:30 UTC749
7ff8adafe9e4atagen2026-10-22 12:30 UTC750
        let response = self
7ff8adafe9e4atagen2026-10-22 12:30 UTC751
            .upload(&content_type, data, None)
7ff8adafe9e4atagen2026-10-22 12:30 UTC752
            .with_send_progress_observable(send_progress)
7ff8adafe9e4atagen2026-10-22 12:30 UTC753
            .await?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC754
        let url = response.content_uri;
7ff8adafe9e4atagen2026-10-22 12:30 UTC755
7ff8adafe9e4atagen2026-10-22 12:30 UTC756
        Ok(Some((MediaSource::Plain(url), thumbnail_info)))
7ff8adafe9e4atagen2026-10-22 12:30 UTC757
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC758
7ff8adafe9e4atagen2026-10-22 12:30 UTC759
    /// Create an [`OwnedMxcUri`] for a file or thumbnail we want to store
7ff8adafe9e4atagen2026-10-22 12:30 UTC760
    /// locally before sending it.
7ff8adafe9e4atagen2026-10-22 12:30 UTC761
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC762
    /// This uses a MXC ID that is only locally valid.
7ff8adafe9e4atagen2026-10-22 12:30 UTC763
    pub(crate) fn make_local_uri(txn_id: &TransactionId) -> OwnedMxcUri {
7ff8adafe9e4atagen2026-10-22 12:30 UTC764
        OwnedMxcUri::from(format!("mxc://{LOCAL_MXC_SERVER_NAME}/{txn_id}"))
7ff8adafe9e4atagen2026-10-22 12:30 UTC765
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC766
7ff8adafe9e4atagen2026-10-22 12:30 UTC767
    /// Create a [`MediaRequest`] for a file we want to store locally before
7ff8adafe9e4atagen2026-10-22 12:30 UTC768
    /// sending it.
7ff8adafe9e4atagen2026-10-22 12:30 UTC769
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC770
    /// This uses a MXC ID that is only locally valid.
7ff8adafe9e4atagen2026-10-22 12:30 UTC771
    pub(crate) fn make_local_file_media_request(txn_id: &TransactionId) -> MediaRequestParameters {
7ff8adafe9e4atagen2026-10-22 12:30 UTC772
        MediaRequestParameters {
7ff8adafe9e4atagen2026-10-22 12:30 UTC773
            source: MediaSource::Plain(Self::make_local_uri(txn_id)),
7ff8adafe9e4atagen2026-10-22 12:30 UTC774
            format: MediaFormat::File,
7ff8adafe9e4atagen2026-10-22 12:30 UTC775
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC776
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC777
7ff8adafe9e4atagen2026-10-22 12:30 UTC778
    /// Returns the local MXC URI contained by the given source, if any.
7ff8adafe9e4atagen2026-10-22 12:30 UTC779
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC780
    /// A local MXC URI is a URI that was generated with `make_local_uri`.
7ff8adafe9e4atagen2026-10-22 12:30 UTC781
    fn as_local_uri(source: &MediaSource) -> Option<&MxcUri> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC782
        let uri = match source {
7ff8adafe9e4atagen2026-10-22 12:30 UTC783
            MediaSource::Plain(uri) => uri,
7ff8adafe9e4atagen2026-10-22 12:30 UTC784
            MediaSource::Encrypted(file) => &file.url,
7ff8adafe9e4atagen2026-10-22 12:30 UTC785
        };
7ff8adafe9e4atagen2026-10-22 12:30 UTC786
7ff8adafe9e4atagen2026-10-22 12:30 UTC787
        uri.server_name()
7ff8adafe9e4atagen2026-10-22 12:30 UTC788
            .is_ok_and(|server_name| server_name == LOCAL_MXC_SERVER_NAME)
7ff8adafe9e4atagen2026-10-22 12:30 UTC789
            .then_some(uri)
7ff8adafe9e4atagen2026-10-22 12:30 UTC790
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC791
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC792
7ff8adafe9e4atagen2026-10-22 12:30 UTC793
#[cfg(test)]
7ff8adafe9e4atagen2026-10-22 12:30 UTC794
mod tests {
7ff8adafe9e4atagen2026-10-22 12:30 UTC795
    use assert_matches2::assert_matches;
7ff8adafe9e4atagen2026-10-22 12:30 UTC796
    use ruma::{
7ff8adafe9e4atagen2026-10-22 12:30 UTC797
        MxcUri,
7ff8adafe9e4atagen2026-10-22 12:30 UTC798
        events::room::{EncryptedFile, MediaSource},
7ff8adafe9e4atagen2026-10-22 12:30 UTC799
        mxc_uri, owned_mxc_uri,
7ff8adafe9e4atagen2026-10-22 12:30 UTC800
    };
7ff8adafe9e4atagen2026-10-22 12:30 UTC801
    use serde_json::json;
7ff8adafe9e4atagen2026-10-22 12:30 UTC802
7ff8adafe9e4atagen2026-10-22 12:30 UTC803
    use super::Media;
7ff8adafe9e4atagen2026-10-22 12:30 UTC804
7ff8adafe9e4atagen2026-10-22 12:30 UTC805
    /// Create an `EncryptedFile` with the given MXC URI.
7ff8adafe9e4atagen2026-10-22 12:30 UTC806
    fn encrypted_file(mxc_uri: &MxcUri) -> Box<EncryptedFile> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC807
        Box::new(
7ff8adafe9e4atagen2026-10-22 12:30 UTC808
            serde_json::from_value(json!({
7ff8adafe9e4atagen2026-10-22 12:30 UTC809
                "url": mxc_uri,
7ff8adafe9e4atagen2026-10-22 12:30 UTC810
                "key": {
7ff8adafe9e4atagen2026-10-22 12:30 UTC811
                    "kty": "oct",
7ff8adafe9e4atagen2026-10-22 12:30 UTC812
                    "key_ops": ["encrypt", "decrypt"],
7ff8adafe9e4atagen2026-10-22 12:30 UTC813
                    "alg": "A256CTR",
7ff8adafe9e4atagen2026-10-22 12:30 UTC814
                    "k": "b50ACIv6LMn9AfMCFD1POJI_UAFWIclxAN1kWrEO2X8",
7ff8adafe9e4atagen2026-10-22 12:30 UTC815
                    "ext": true,
7ff8adafe9e4atagen2026-10-22 12:30 UTC816
                },
7ff8adafe9e4atagen2026-10-22 12:30 UTC817
                "iv": "AK1wyzigZtQAAAABAAAAKK",
7ff8adafe9e4atagen2026-10-22 12:30 UTC818
                "hashes": {
7ff8adafe9e4atagen2026-10-22 12:30 UTC819
                    "sha256": "/NogKqW5bz/m8xHgFiH5haFGjCNVmUIPLzfvOhHdrxY",
7ff8adafe9e4atagen2026-10-22 12:30 UTC820
                },
7ff8adafe9e4atagen2026-10-22 12:30 UTC821
                "v": "v2",
7ff8adafe9e4atagen2026-10-22 12:30 UTC822
            }))
7ff8adafe9e4atagen2026-10-22 12:30 UTC823
            .unwrap(),
7ff8adafe9e4atagen2026-10-22 12:30 UTC824
        )
7ff8adafe9e4atagen2026-10-22 12:30 UTC825
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC826
7ff8adafe9e4atagen2026-10-22 12:30 UTC827
    #[test]
7ff8adafe9e4atagen2026-10-22 12:30 UTC828
    fn test_as_local_uri() {
7ff8adafe9e4atagen2026-10-22 12:30 UTC829
        let txn_id = "abcdef";
7ff8adafe9e4atagen2026-10-22 12:30 UTC830
7ff8adafe9e4atagen2026-10-22 12:30 UTC831
        // Request generated with `make_local_file_media_request`.
7ff8adafe9e4atagen2026-10-22 12:30 UTC832
        let request = Media::make_local_file_media_request(txn_id.into());
7ff8adafe9e4atagen2026-10-22 12:30 UTC833
        assert_matches!(Media::as_local_uri(&request.source), Some(uri));
7ff8adafe9e4atagen2026-10-22 12:30 UTC834
        assert_eq!(uri.media_id(), Ok(txn_id));
7ff8adafe9e4atagen2026-10-22 12:30 UTC835
7ff8adafe9e4atagen2026-10-22 12:30 UTC836
        // Local plain source.
7ff8adafe9e4atagen2026-10-22 12:30 UTC837
        let source = MediaSource::Plain(Media::make_local_uri(txn_id.into()));
7ff8adafe9e4atagen2026-10-22 12:30 UTC838
        assert_matches!(Media::as_local_uri(&source), Some(uri));
7ff8adafe9e4atagen2026-10-22 12:30 UTC839
        assert_eq!(uri.media_id(), Ok(txn_id));
7ff8adafe9e4atagen2026-10-22 12:30 UTC840
7ff8adafe9e4atagen2026-10-22 12:30 UTC841
        // Local encrypted source.
7ff8adafe9e4atagen2026-10-22 12:30 UTC842
        let source = MediaSource::Encrypted(encrypted_file(&Media::make_local_uri(txn_id.into())));
7ff8adafe9e4atagen2026-10-22 12:30 UTC843
        assert_matches!(Media::as_local_uri(&source), Some(uri));
7ff8adafe9e4atagen2026-10-22 12:30 UTC844
        assert_eq!(uri.media_id(), Ok(txn_id));
7ff8adafe9e4atagen2026-10-22 12:30 UTC845
7ff8adafe9e4atagen2026-10-22 12:30 UTC846
        // Test non-local plain source.
7ff8adafe9e4atagen2026-10-22 12:30 UTC847
        let source = MediaSource::Plain(owned_mxc_uri!("mxc://server.local/poiuyt"));
7ff8adafe9e4atagen2026-10-22 12:30 UTC848
        assert_matches!(Media::as_local_uri(&source), None);
7ff8adafe9e4atagen2026-10-22 12:30 UTC849
7ff8adafe9e4atagen2026-10-22 12:30 UTC850
        // Test non-local encrypted source.
7ff8adafe9e4atagen2026-10-22 12:30 UTC851
        let source = MediaSource::Encrypted(encrypted_file(mxc_uri!("mxc://server.local/mlkjhg")));
7ff8adafe9e4atagen2026-10-22 12:30 UTC852
        assert_matches!(Media::as_local_uri(&source), None);
7ff8adafe9e4atagen2026-10-22 12:30 UTC853
7ff8adafe9e4atagen2026-10-22 12:30 UTC854
        // Test invalid MXC URI.
7ff8adafe9e4atagen2026-10-22 12:30 UTC855
        let source = MediaSource::Plain("https://server.local/nbvcxw".into());
7ff8adafe9e4atagen2026-10-22 12:30 UTC856
        assert_matches!(Media::as_local_uri(&source), None);
7ff8adafe9e4atagen2026-10-22 12:30 UTC857
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC858
}