Skip to main content

matrix_sdk/
media.rs

1// Copyright 2021 Kévin Commaille
2// Copyright 2022 The Matrix.org Foundation C.I.C.
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! High-level media API.
17
18#[cfg(feature = "e2e-encryption")]
19use std::io::Read;
20use std::{fmt, time::Duration};
21#[cfg(not(target_family = "wasm"))]
22use std::{fs::File, path::Path};
23
24use eyeball::SharedObservable;
25use futures_util::future::try_join;
26use matrix_sdk_base::media::store::IgnoreMediaRetentionPolicy;
27pub use matrix_sdk_base::media::{store::MediaRetentionPolicy, *};
28use matrix_sdk_common::{BoxFuture, SendOutsideWasm, SyncOutsideWasm};
29use mime::Mime;
30use ruma::{
31    MilliSecondsSinceUnixEpoch, MxcUri, OwnedMxcUri, TransactionId, UInt,
32    api::{
33        Metadata,
34        client::{authenticated_media, media},
35        error::ErrorKind,
36    },
37    assign,
38    events::room::{MediaSource, ThumbnailInfo},
39};
40use serde_json::value::RawValue as RawJsonValue;
41#[cfg(not(target_family = "wasm"))]
42use tempfile::{Builder as TempFileBuilder, NamedTempFile, TempDir};
43#[cfg(not(target_family = "wasm"))]
44use tokio::{fs::File as TokioFile, io::AsyncWriteExt};
45
46use crate::{
47    Client, Error, Result, TransmissionProgress, attachment::Thumbnail,
48    client::futures::SendMediaUploadRequest, config::RequestConfig,
49};
50
51/// A conservative upload speed of 1Mbps
52const DEFAULT_UPLOAD_SPEED: u64 = 125_000;
53/// 5 min minimal upload request timeout, used to clamp the request timeout.
54const MIN_UPLOAD_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 5);
55/// The server name used to generate local MXC URIs.
56// This mustn't represent a potentially valid media server, otherwise it'd be
57// possible for an attacker to return malicious content under some
58// preconditions (e.g. the cache store has been cleared before the upload
59// took place). To mitigate against this, we use the .localhost TLD,
60// which is guaranteed to be on the local machine. As a result, the only attack
61// possible would be coming from the user themselves, which we consider a
62// non-threat.
63const LOCAL_MXC_SERVER_NAME: &str = "send-queue.localhost";
64
65/// A high-level API to interact with the media API.
66#[derive(Debug, Clone)]
67pub struct Media {
68    /// The underlying HTTP client.
69    client: Client,
70}
71
72/// A file handle that takes ownership of a media file on disk. When the handle
73/// is dropped, the file will be removed from the disk.
74#[derive(Debug)]
75#[cfg(not(target_family = "wasm"))]
76pub struct MediaFileHandle {
77    /// The temporary file that contains the media.
78    file: NamedTempFile,
79    /// An intermediary temporary directory used in certain cases.
80    ///
81    /// Only stored for its `Drop` semantics.
82    _directory: Option<TempDir>,
83}
84
85#[cfg(not(target_family = "wasm"))]
86impl MediaFileHandle {
87    /// Get the media file's path.
88    pub fn path(&self) -> &Path {
89        self.file.path()
90    }
91
92    /// Persist the media file to the given path.
93    pub fn persist(self, path: &Path) -> Result<File, PersistError> {
94        self.file.persist(path).map_err(|e| PersistError {
95            error: e.error,
96            file: Self { file: e.file, _directory: self._directory },
97        })
98    }
99}
100
101/// Error returned when [`MediaFileHandle::persist`] fails.
102#[cfg(not(target_family = "wasm"))]
103pub struct PersistError {
104    /// The underlying IO error.
105    pub error: std::io::Error,
106    /// The temporary file that couldn't be persisted.
107    pub file: MediaFileHandle,
108}
109
110#[cfg(not(any(target_family = "wasm", tarpaulin_include)))]
111impl fmt::Debug for PersistError {
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        write!(f, "PersistError({:?})", self.error)
114    }
115}
116
117#[cfg(not(any(target_family = "wasm", tarpaulin_include)))]
118impl fmt::Display for PersistError {
119    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120        write!(f, "failed to persist temporary file: {}", self.error)
121    }
122}
123
124/// A preallocated MXC URI created by [`Media::create_content_uri()`], and
125/// to be used with [`Media::upload_preallocated()`].
126#[derive(Debug)]
127pub struct PreallocatedMxcUri {
128    /// The URI for the media URI.
129    pub uri: OwnedMxcUri,
130    /// The expiration date for the media URI.
131    expire_date: Option<MilliSecondsSinceUnixEpoch>,
132}
133
134/// An error that happened in the realm of media.
135#[derive(Debug, thiserror::Error)]
136pub enum MediaError {
137    /// A preallocated MXC URI has expired.
138    #[error("a preallocated MXC URI has expired")]
139    ExpiredPreallocatedMxcUri,
140
141    /// Preallocated media already had content, cannot overwrite.
142    #[error("preallocated media already had content, cannot overwrite")]
143    CannotOverwriteMedia,
144
145    /// Local-only media content was not found.
146    #[error("local-only media content was not found")]
147    LocalMediaNotFound,
148
149    /// The provided media is too large to upload.
150    #[error(
151        "The provided media is too large to upload. \
152         Maximum upload length is {max} bytes, tried to upload {current} bytes"
153    )]
154    MediaTooLargeToUpload {
155        /// The `max_upload_size` value for this homeserver.
156        max: UInt,
157        /// The size of the current media to upload.
158        current: UInt,
159    },
160
161    /// Fetching the `max_upload_size` value from the homeserver failed.
162    #[error("Fetching the `max_upload_size` value from the homeserver failed: {0}")]
163    FetchMaxUploadSizeFailed(String),
164}
165
166/// A generic trait for fetching media content.
167pub trait MediaFetcher: SendOutsideWasm + SyncOutsideWasm + fmt::Debug {
168    /// Fetches the media content for the given [`MediaRequestParameters`].
169    /// Returns either a byte array or an [`crate::Error`].
170    fn fetch_media_content<'a>(
171        &'a self,
172        client: &'a Client,
173        request: &'a MediaRequestParameters,
174    ) -> BoxFuture<'a, Result<Vec<u8>, Error>>;
175}
176
177impl Media {
178    pub(crate) fn new(client: Client) -> Self {
179        Self { client }
180    }
181
182    /// Upload some media to the server.
183    ///
184    /// # Arguments
185    ///
186    /// * `content_type` - The type of the media, this will be used as the
187    ///   content-type header.
188    ///
189    /// * `data` - Vector of bytes to be uploaded to the server.
190    ///
191    /// * `request_config` - Optional request configuration for the HTTP client,
192    ///   overriding the default. If not provided, a reasonable timeout value is
193    ///   inferred.
194    ///
195    /// # Examples
196    ///
197    /// ```no_run
198    /// # use std::fs;
199    /// # use matrix_sdk::{Client, ruma::room_id};
200    /// # use url::Url;
201    /// # use mime;
202    /// # async {
203    /// # let homeserver = Url::parse("http://localhost:8080")?;
204    /// # let mut client = Client::new(homeserver).await?;
205    /// let image = fs::read("/home/example/my-cat.jpg")?;
206    ///
207    /// let response =
208    ///     client.media().upload(&mime::IMAGE_JPEG, image, None).await?;
209    ///
210    /// println!("Cat URI: {}", response.content_uri);
211    /// # anyhow::Ok(()) };
212    /// ```
213    pub fn upload(
214        &self,
215        content_type: &Mime,
216        data: Vec<u8>,
217        request_config: Option<RequestConfig>,
218    ) -> SendMediaUploadRequest {
219        let request_config = request_config.unwrap_or_else(|| {
220            self.client.request_config().timeout(Self::reasonable_upload_timeout(&data))
221        });
222
223        let request = assign!(media::create_content::v3::Request::new(data), {
224            content_type: Some(content_type.essence_str().to_owned()),
225        });
226
227        let request = self.client.send(request).with_request_config(request_config);
228        SendMediaUploadRequest::new(request)
229    }
230
231    /// Returns a reasonable upload timeout for an upload, based on the size of
232    /// the data to be uploaded.
233    pub(crate) fn reasonable_upload_timeout(data: &[u8]) -> Duration {
234        std::cmp::max(
235            Duration::from_secs(data.len() as u64 / DEFAULT_UPLOAD_SPEED),
236            MIN_UPLOAD_REQUEST_TIMEOUT,
237        )
238    }
239
240    /// Preallocates an MXC URI for a media that will be uploaded soon.
241    ///
242    /// This preallocates an URI *before* any content is uploaded to the server.
243    /// The resulting preallocated MXC URI can then be consumed with
244    /// [`Media::upload_preallocated`].
245    ///
246    /// # Examples
247    ///
248    /// ```no_run
249    /// # use std::fs;
250    /// # use matrix_sdk::{Client, ruma::room_id};
251    /// # use url::Url;
252    /// # use mime;
253    /// # async {
254    /// # let homeserver = Url::parse("http://localhost:8080")?;
255    /// # let mut client = Client::new(homeserver).await?;
256    ///
257    /// let preallocated = client.media().create_content_uri().await?;
258    /// println!("Cat URI: {}", preallocated.uri);
259    ///
260    /// let image = fs::read("/home/example/my-cat.jpg")?;
261    /// client
262    ///     .media()
263    ///     .upload_preallocated(preallocated, &mime::IMAGE_JPEG, image)
264    ///     .await?;
265    ///
266    /// # anyhow::Ok(()) };
267    /// ```
268    pub async fn create_content_uri(&self) -> Result<PreallocatedMxcUri> {
269        // Note: this request doesn't have any parameters.
270        let request = media::create_mxc_uri::v1::Request::default();
271
272        let response = self.client.send(request).await?;
273
274        Ok(PreallocatedMxcUri {
275            uri: response.content_uri,
276            expire_date: response.unused_expires_at,
277        })
278    }
279
280    /// Fills the content of a preallocated MXC URI with the given content type
281    /// and data.
282    ///
283    /// The URI must have been preallocated with [`Self::create_content_uri`].
284    /// See this method's documentation for a full example.
285    pub async fn upload_preallocated(
286        &self,
287        uri: PreallocatedMxcUri,
288        content_type: &Mime,
289        data: Vec<u8>,
290    ) -> Result<()> {
291        // Do a best-effort at reporting an expired MXC URI here; otherwise the server
292        // may complain about it later.
293        if let Some(expire_date) = uri.expire_date
294            && MilliSecondsSinceUnixEpoch::now() >= expire_date
295        {
296            return Err(Error::Media(MediaError::ExpiredPreallocatedMxcUri));
297        }
298
299        let timeout = std::cmp::max(
300            Duration::from_secs(data.len() as u64 / DEFAULT_UPLOAD_SPEED),
301            MIN_UPLOAD_REQUEST_TIMEOUT,
302        );
303
304        let request = assign!(media::create_content_async::v3::Request::from_url(&uri.uri, data)?, {
305            content_type: Some(content_type.as_ref().to_owned()),
306        });
307
308        let request_config = self.client.request_config().timeout(timeout);
309
310        if let Err(err) = self.client.send(request).with_request_config(request_config).await {
311            match err.client_api_error_kind() {
312                Some(ErrorKind::CannotOverwriteMedia) => {
313                    Err(Error::Media(MediaError::CannotOverwriteMedia))
314                }
315
316                // Unfortunately, the spec says a server will return 404 for either an expired MXC
317                // ID or a non-existing MXC ID. Do a best-effort guess to recognize an expired MXC
318                // ID based on the error string, which will work with Synapse (as of 2024-10-23).
319                Some(ErrorKind::Unknown) if err.to_string().contains("expired") => {
320                    Err(Error::Media(MediaError::ExpiredPreallocatedMxcUri))
321                }
322
323                _ => Err(err.into()),
324            }
325        } else {
326            Ok(())
327        }
328    }
329
330    /// Gets a media file by copying it to a temporary location on disk.
331    ///
332    /// The file won't be encrypted even if it is encrypted on the server.
333    ///
334    /// Returns a `MediaFileHandle` which takes ownership of the file. When the
335    /// handle is dropped, the file will be deleted from the temporary location.
336    ///
337    /// # Arguments
338    ///
339    /// * `request` - The `MediaRequest` of the content.
340    ///
341    /// * `filename` - The filename specified in the event. It is suggested to
342    ///   use the `filename()` method on the event's content instead of using
343    ///   the `filename` field directly. If not provided, a random name will be
344    ///   generated.
345    ///
346    /// * `content_type` - The type of the media, this will be used to set the
347    ///   temporary file's extension when one isn't included in the filename.
348    ///
349    /// * `use_cache` - If we should use the media cache for this request.
350    ///
351    /// * `temp_dir` - Path to a directory where temporary directories can be
352    ///   created. If not provided, a default, global temporary directory will
353    ///   be used; this may not work properly on Android, where the default
354    ///   location may require root access on some older Android versions.
355    #[cfg(not(target_family = "wasm"))]
356    pub async fn get_media_file(
357        &self,
358        request: &MediaRequestParameters,
359        filename: Option<String>,
360        content_type: &Mime,
361        use_cache: bool,
362        temp_dir: Option<String>,
363    ) -> Result<MediaFileHandle> {
364        let data = self.get_media_content(request, use_cache).await?;
365
366        let inferred_extension = mime2ext::mime2ext(content_type);
367
368        let filename_as_path = filename.as_ref().map(Path::new);
369
370        let (sanitized_filename, filename_has_extension) = if let Some(path) = filename_as_path {
371            let sanitized_filename = path.file_name().and_then(|f| f.to_str());
372            let filename_has_extension = path.extension().is_some();
373            (sanitized_filename, filename_has_extension)
374        } else {
375            (None, false)
376        };
377
378        let (temp_file, temp_dir) =
379            match (sanitized_filename, filename_has_extension, inferred_extension) {
380                // If the file name has an extension use that
381                (Some(filename_with_extension), true, _) => {
382                    // Use an intermediary directory to avoid conflicts
383                    let temp_dir = temp_dir.map(TempDir::new_in).unwrap_or_else(TempDir::new)?;
384                    let temp_file = TempFileBuilder::new()
385                        .prefix(filename_with_extension)
386                        .rand_bytes(0)
387                        .tempfile_in(&temp_dir)?;
388                    (temp_file, Some(temp_dir))
389                }
390                // If the file name doesn't have an extension try inferring one for it
391                (Some(filename), false, Some(inferred_extension)) => {
392                    // Use an intermediary directory to avoid conflicts
393                    let temp_dir = temp_dir.map(TempDir::new_in).unwrap_or_else(TempDir::new)?;
394                    let temp_file = TempFileBuilder::new()
395                        .prefix(filename)
396                        .suffix(&(".".to_owned() + inferred_extension))
397                        .rand_bytes(0)
398                        .tempfile_in(&temp_dir)?;
399                    (temp_file, Some(temp_dir))
400                }
401                // If the only thing we have is an inferred extension then use that together with a
402                // randomly generated file name
403                (None, _, Some(inferred_extension)) => (
404                    TempFileBuilder::new()
405                        .suffix(&&(".".to_owned() + inferred_extension))
406                        .tempfile()?,
407                    None,
408                ),
409                // Otherwise just use a completely random file name
410                _ => (TempFileBuilder::new().tempfile()?, None),
411            };
412
413        let mut file = TokioFile::from_std(temp_file.reopen()?);
414        file.write_all(&data).await?;
415        // Make sure the file metadata is flushed to disk.
416        file.sync_all().await?;
417
418        Ok(MediaFileHandle { file: temp_file, _directory: temp_dir })
419    }
420
421    /// Get a media file's content.
422    ///
423    /// If the content is encrypted and encryption is enabled, the content will
424    /// be decrypted.
425    ///
426    /// # Arguments
427    ///
428    /// * `request` - The `MediaRequest` of the content.
429    ///
430    /// * `use_cache` - If we should use the media cache for this request.
431    pub async fn get_media_content(
432        &self,
433        request: &MediaRequestParameters,
434        use_cache: bool,
435    ) -> Result<Vec<u8>> {
436        // This is a local media. Force to read the media's content from the store: it
437        // cannot exist somewhere else!
438        if Self::is_local_uri(&request.source) {
439            // Local medias are always cached with `MediaFormat::File`, be it the file
440            // itself or its thumbnail (see `RoomSendQueue::cache_media`), so ignore the
441            // requested format.
442            let request = &MediaRequestParameters {
443                source: request.source.clone(),
444                format: MediaFormat::File,
445            };
446
447            if let Some(content) =
448                self.client.media_store().lock().await?.get_media_content(request).await?
449            {
450                return Ok(content);
451            } else {
452                return Err(Error::MediaStore(Box::new(store::MediaStoreError::InvalidData {
453                    details: format!("Media does not exist: `{request:?}`"),
454                })));
455            }
456        }
457
458        // Read from the cache: if it doesn't exist, the execution continues by reading
459        // the media from the network.
460        if use_cache
461            && let Some(content) =
462                self.client.media_store().lock().await?.get_media_content(request).await?
463        {
464            return Ok(content);
465        }
466
467        let content = self
468            .client
469            .inner
470            .media_fetcher
471            .read()
472            .await
473            .fetch_media_content(&self.client, request)
474            .await?;
475
476        if use_cache {
477            self.client
478                .media_store()
479                .lock()
480                .await?
481                .add_media_content(request, content.clone(), IgnoreMediaRetentionPolicy::No)
482                .await?;
483        }
484
485        Ok(content)
486    }
487
488    /// Remove a media file's content from the store.
489    ///
490    /// # Arguments
491    ///
492    /// * `request` - The `MediaRequest` of the content.
493    pub async fn remove_media_content(&self, request: &MediaRequestParameters) -> Result<()> {
494        Ok(self.client.media_store().lock().await?.remove_media_content(request).await?)
495    }
496
497    /// Delete all the media content corresponding to the given
498    /// uri from the store.
499    ///
500    /// # Arguments
501    ///
502    /// * `uri` - The `MxcUri` of the files.
503    pub async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()> {
504        Ok(self.client.media_store().lock().await?.remove_media_content_for_uri(uri).await?)
505    }
506
507    /// Get the file of the given media event content.
508    ///
509    /// If the content is encrypted and encryption is enabled, the content will
510    /// be decrypted.
511    ///
512    /// Returns `Ok(None)` if the event content has no file.
513    ///
514    /// This is a convenience method that calls the
515    /// [`get_media_content`](#method.get_media_content) method.
516    ///
517    /// # Arguments
518    ///
519    /// * `event_content` - The media event content.
520    ///
521    /// * `use_cache` - If we should use the media cache for this file.
522    pub async fn get_file(
523        &self,
524        event_content: &impl MediaEventContent,
525        use_cache: bool,
526    ) -> Result<Option<Vec<u8>>> {
527        let Some(source) = event_content.source() else { return Ok(None) };
528        let file = self
529            .get_media_content(
530                &MediaRequestParameters { source, format: MediaFormat::File },
531                use_cache,
532            )
533            .await?;
534        Ok(Some(file))
535    }
536
537    /// Remove the file of the given media event content from the cache.
538    ///
539    /// This is a convenience method that calls the
540    /// [`remove_media_content`](#method.remove_media_content) method.
541    ///
542    /// # Arguments
543    ///
544    /// * `event_content` - The media event content.
545    pub async fn remove_file(&self, event_content: &impl MediaEventContent) -> Result<()> {
546        if let Some(source) = event_content.source() {
547            self.remove_media_content(&MediaRequestParameters {
548                source,
549                format: MediaFormat::File,
550            })
551            .await?;
552        }
553
554        Ok(())
555    }
556
557    /// Get a thumbnail of the given media event content.
558    ///
559    /// If the content is encrypted and encryption is enabled, the content will
560    /// be decrypted.
561    ///
562    /// Returns `Ok(None)` if the event content has no thumbnail.
563    ///
564    /// This is a convenience method that calls the
565    /// [`get_media_content`](#method.get_media_content) method.
566    ///
567    /// # Arguments
568    ///
569    /// * `event_content` - The media event content.
570    ///
571    /// * `settings` - The _desired_ settings of the thumbnail. The actual
572    ///   thumbnail may not match the settings specified.
573    ///
574    /// * `use_cache` - If we should use the media cache for this thumbnail.
575    pub async fn get_thumbnail(
576        &self,
577        event_content: &impl MediaEventContent,
578        settings: MediaThumbnailSettings,
579        use_cache: bool,
580    ) -> Result<Option<Vec<u8>>> {
581        let Some(source) = event_content.thumbnail_source() else { return Ok(None) };
582        let thumbnail = self
583            .get_media_content(
584                &MediaRequestParameters { source, format: MediaFormat::Thumbnail(settings) },
585                use_cache,
586            )
587            .await?;
588        Ok(Some(thumbnail))
589    }
590
591    /// Remove the thumbnail of the given media event content from the cache.
592    ///
593    /// This is a convenience method that calls the
594    /// [`remove_media_content`](#method.remove_media_content) method.
595    ///
596    /// # Arguments
597    ///
598    /// * `event_content` - The media event content.
599    ///
600    /// * `size` - The _desired_ settings of the thumbnail. Must match the
601    ///   settings requested with [`get_thumbnail`](#method.get_thumbnail).
602    pub async fn remove_thumbnail(
603        &self,
604        event_content: &impl MediaEventContent,
605        settings: MediaThumbnailSettings,
606    ) -> Result<()> {
607        if let Some(source) = event_content.source() {
608            self.remove_media_content(&MediaRequestParameters {
609                source,
610                format: MediaFormat::Thumbnail(settings),
611            })
612            .await?
613        }
614
615        Ok(())
616    }
617
618    /// Get a preview for a URL, as OpenGraph-like data.
619    ///
620    /// This is generated by the homeserver, which fetches the URL itself. Note
621    /// that servers may disable this endpoint entirely, in which case this
622    /// returns an error, and that using it in an encrypted room discloses the
623    /// URL to the homeserver.
624    ///
625    /// Uses the authenticated endpoint when the homeserver supports it,
626    /// falling back to the deprecated unauthenticated one otherwise.
627    ///
628    /// # Arguments
629    ///
630    /// * `url` - The URL to get a preview of.
631    ///
632    /// * `ts` - The preferred point in time to return a preview for, if the
633    ///   homeserver supports returning previews for a given point in time.
634    ///
635    /// # Returns
636    ///
637    /// The OpenGraph-like data for the URL, if the homeserver returned any. It
638    /// is returned as raw JSON, since the fields are not a fixed set: they
639    /// mirror OpenGraph, with the addition of `matrix:image:size` for the
640    /// image size in bytes, and `og:image` holding an MXC URI rather than an
641    /// HTTP URL.
642    ///
643    /// # Examples
644    ///
645    /// ```no_run
646    /// # use matrix_sdk::Client;
647    /// # use url::Url;
648    /// # async {
649    /// # let homeserver = Url::parse("http://localhost:8080")?;
650    /// # let client = Client::new(homeserver).await?;
651    /// if let Some(data) =
652    ///     client.media().get_media_preview("https://matrix.org", None).await?
653    /// {
654    ///     println!("Preview data: {}", data.get());
655    /// }
656    /// # anyhow::Ok(()) };
657    /// ```
658    pub async fn get_media_preview(
659        &self,
660        url: &str,
661        ts: Option<MilliSecondsSinceUnixEpoch>,
662    ) -> Result<Option<Box<RawJsonValue>>> {
663        // Use the authenticated endpoint when the server supports it.
664        let supported_versions = self.client.supported_versions().await?;
665
666        let use_auth = authenticated_media::get_media_preview::v1::Request::PATH_BUILDER
667            .is_supported(&supported_versions);
668
669        if use_auth {
670            let mut request =
671                authenticated_media::get_media_preview::v1::Request::new(url.to_owned());
672            request.ts = ts;
673
674            Ok(self.client.send(request).await?.data)
675        } else {
676            // The whole block is `allow(deprecated)`: both the endpoint and its
677            // `ts` field are deprecated since Matrix 1.11, and we only reach
678            // this branch when the homeserver is too old for the authenticated
679            // endpoint above.
680            #[allow(deprecated)]
681            {
682                let mut request = media::get_media_preview::v3::Request::new(url.to_owned());
683                request.ts = ts;
684
685                Ok(self.client.send(request).await?.data)
686            }
687        }
688    }
689
690    /// Set the [`MediaRetentionPolicy`] to use for deciding whether to store or
691    /// keep media content.
692    ///
693    /// It is used:
694    ///
695    /// * When a media needs to be cached, to check that it does not exceed the
696    ///   max file size.
697    ///
698    /// * When [`Media::clean()`], to check that all media content in the store
699    ///   fits those criteria.
700    ///
701    /// To apply the new policy to the media cache right away,
702    /// [`Media::clean()`] should be called after this.
703    ///
704    /// By default, an empty `MediaRetentionPolicy` is used, which means that no
705    /// criteria are applied.
706    ///
707    /// # Arguments
708    ///
709    /// * `policy` - The `MediaRetentionPolicy` to use.
710    pub async fn set_media_retention_policy(&self, policy: MediaRetentionPolicy) -> Result<()> {
711        self.client.media_store().lock().await?.set_media_retention_policy(policy).await?;
712        Ok(())
713    }
714
715    /// Get the current `MediaRetentionPolicy`.
716    pub async fn media_retention_policy(&self) -> Result<MediaRetentionPolicy> {
717        Ok(self.client.media_store().lock().await?.media_retention_policy())
718    }
719
720    /// Clean up the media cache with the current [`MediaRetentionPolicy`].
721    ///
722    /// If there is already an ongoing cleanup, this is a noop.
723    pub async fn clean(&self) -> Result<()> {
724        self.client.media_store().lock().await?.clean().await?;
725        Ok(())
726    }
727
728    /// Upload the file bytes in `data` and return the source information.
729    pub(crate) async fn upload_plain_media_and_thumbnail(
730        &self,
731        content_type: &Mime,
732        data: Vec<u8>,
733        thumbnail: Option<Thumbnail>,
734        send_progress: SharedObservable<TransmissionProgress>,
735    ) -> Result<(MediaSource, Option<(MediaSource, Box<ThumbnailInfo>)>)> {
736        let upload_thumbnail = self.upload_thumbnail(thumbnail, send_progress.clone());
737
738        let upload_attachment = async move {
739            self.upload(content_type, data, None).with_send_progress_observable(send_progress).await
740        };
741
742        let (thumbnail, response) = try_join(upload_thumbnail, upload_attachment).await?;
743
744        Ok((MediaSource::Plain(response.content_uri), thumbnail))
745    }
746
747    /// Uploads an unencrypted thumbnail to the media repository, and returns
748    /// its source and extra information.
749    async fn upload_thumbnail(
750        &self,
751        thumbnail: Option<Thumbnail>,
752        send_progress: SharedObservable<TransmissionProgress>,
753    ) -> Result<Option<(MediaSource, Box<ThumbnailInfo>)>> {
754        let Some(thumbnail) = thumbnail else {
755            return Ok(None);
756        };
757
758        let (data, content_type, thumbnail_info) = thumbnail.into_parts();
759
760        let response = self
761            .upload(&content_type, data, None)
762            .with_send_progress_observable(send_progress)
763            .await?;
764        let url = response.content_uri;
765
766        Ok(Some((MediaSource::Plain(url), thumbnail_info)))
767    }
768
769    /// Create an [`OwnedMxcUri`] for a file or thumbnail we want to store
770    /// locally before sending it.
771    ///
772    /// This uses a MXC ID that is only locally valid.
773    pub(crate) fn make_local_uri(txn_id: &TransactionId) -> OwnedMxcUri {
774        OwnedMxcUri::from(format!("mxc://{LOCAL_MXC_SERVER_NAME}/{txn_id}"))
775    }
776
777    /// Create a [`MediaRequest`] for a file we want to store locally before
778    /// sending it.
779    ///
780    /// This uses a MXC ID that is only locally valid.
781    pub(crate) fn make_local_file_media_request(txn_id: &TransactionId) -> MediaRequestParameters {
782        MediaRequestParameters {
783            source: MediaSource::Plain(Self::make_local_uri(txn_id)),
784            format: MediaFormat::File,
785        }
786    }
787
788    /// Checks whether the MXC represents a local URI.
789    ///
790    /// A local MXC URI is a URI that was generated with
791    /// [`Self::make_local_uri`].
792    fn is_local_uri(source: &MediaSource) -> bool {
793        let uri = match source {
794            MediaSource::Plain(uri) => uri,
795            MediaSource::Encrypted(file) => &file.url,
796        };
797
798        uri.server_name().is_ok_and(|server_name| server_name == LOCAL_MXC_SERVER_NAME)
799    }
800}
801
802/// A [`MediaFetcher`] that uses the default media/authenticated media endpoints
803/// to fetch new media.
804#[derive(Debug, Clone)]
805pub struct DefaultMediaFetcher;
806
807impl MediaFetcher for DefaultMediaFetcher {
808    fn fetch_media_content<'a>(
809        &'a self,
810        client: &'a Client,
811        request: &'a MediaRequestParameters,
812    ) -> BoxFuture<'a, Result<Vec<u8>, Error>> {
813        Box::pin(async move {
814            let request_config = client
815                .request_config()
816                // Downloading a file should have no timeout as we don't know the network
817                // connectivity available for the user or the file size
818                .timeout(Some(Duration::MAX));
819
820            // Use the authenticated endpoints when the server supports it.
821            let supported_versions = client.supported_versions().await?;
822
823            let use_auth = authenticated_media::get_content::v1::Request::PATH_BUILDER
824                .is_supported(&supported_versions);
825
826            match &request.source {
827                MediaSource::Encrypted(file) => {
828                    let content = if use_auth {
829                        let request =
830                            authenticated_media::get_content::v1::Request::from_uri(&file.url)?;
831                        client.send(request).with_request_config(request_config).await?.file
832                    } else {
833                        #[allow(deprecated)]
834                        let request = media::get_content::v3::Request::from_url(&file.url)?;
835                        client.send(request).with_request_config(request_config).await?.file
836                    };
837
838                    #[cfg(feature = "e2e-encryption")]
839                    let content = {
840                        let content_len = content.len();
841                        let mut cursor = std::io::Cursor::new(content);
842                        let mut reader = matrix_sdk_base::crypto::AttachmentDecryptor::new(
843                            &mut cursor,
844                            file.as_ref().clone().into(),
845                        )?;
846
847                        // Encrypted size should be the same as the decrypted size,
848                        // rounded up to a cipher block.
849                        let mut decrypted = Vec::with_capacity(content_len);
850
851                        reader.read_to_end(&mut decrypted)?;
852
853                        decrypted
854                    };
855
856                    Ok(content)
857                }
858
859                MediaSource::Plain(uri) => {
860                    if let MediaFormat::Thumbnail(settings) = &request.format {
861                        if use_auth {
862                            let mut request =
863                                authenticated_media::get_content_thumbnail::v1::Request::from_uri(
864                                    uri,
865                                    settings.width,
866                                    settings.height,
867                                )?;
868                            request.method = Some(settings.method.clone());
869                            request.animated = Some(settings.animated);
870
871                            Ok(client.send(request).with_request_config(request_config).await?.file)
872                        } else {
873                            #[allow(deprecated)]
874                            let request = {
875                                let mut request =
876                                    media::get_content_thumbnail::v3::Request::from_url(
877                                        uri,
878                                        settings.width,
879                                        settings.height,
880                                    )?;
881                                request.method = Some(settings.method.clone());
882                                request.animated = Some(settings.animated);
883                                request
884                            };
885
886                            Ok(client.send(request).with_request_config(request_config).await?.file)
887                        }
888                    } else if use_auth {
889                        let request = authenticated_media::get_content::v1::Request::from_uri(uri)?;
890                        Ok(client.send(request).with_request_config(request_config).await?.file)
891                    } else {
892                        #[allow(deprecated)]
893                        let request = media::get_content::v3::Request::from_url(uri)?;
894                        Ok(client.send(request).with_request_config(request_config).await?.file)
895                    }
896                }
897            }
898        })
899    }
900}
901
902#[cfg(test)]
903mod tests {
904    use std::ops::Not;
905
906    use ruma::{
907        MxcUri,
908        events::room::{EncryptedFile, MediaSource},
909        mxc_uri, owned_mxc_uri,
910    };
911    use serde_json::json;
912
913    use super::Media;
914
915    /// Create an `EncryptedFile` with the given MXC URI.
916    fn encrypted_file(mxc_uri: &MxcUri) -> Box<EncryptedFile> {
917        Box::new(
918            serde_json::from_value(json!({
919                "url": mxc_uri,
920                "key": {
921                    "kty": "oct",
922                    "key_ops": ["encrypt", "decrypt"],
923                    "alg": "A256CTR",
924                    "k": "b50ACIv6LMn9AfMCFD1POJI_UAFWIclxAN1kWrEO2X8",
925                    "ext": true,
926                },
927                "iv": "AK1wyzigZtQAAAABAAAAKK",
928                "hashes": {
929                    "sha256": "/NogKqW5bz/m8xHgFiH5haFGjCNVmUIPLzfvOhHdrxY",
930                },
931                "v": "v2",
932            }))
933            .unwrap(),
934        )
935    }
936
937    #[test]
938    fn test_make_local_uri() {
939        let txn_id = "abcdef";
940
941        let uri = Media::make_local_uri(txn_id.into());
942        assert_eq!(uri.media_id().unwrap(), txn_id);
943    }
944
945    #[test]
946    fn test_is_local_uri() {
947        let txn_id = "abcdef";
948
949        // Request generated with `make_local_file_media_request`.
950        let request = Media::make_local_file_media_request(txn_id.into());
951        assert!(Media::is_local_uri(&request.source));
952
953        // Local plain source.
954        let source = MediaSource::Plain(Media::make_local_uri(txn_id.into()));
955        assert!(Media::is_local_uri(&source));
956
957        // Local encrypted source.
958        let source = MediaSource::Encrypted(encrypted_file(&Media::make_local_uri(txn_id.into())));
959        assert!(Media::is_local_uri(&source));
960
961        // Test non-local plain source.
962        let source = MediaSource::Plain(owned_mxc_uri!("mxc://server.local/poiuyt"));
963        assert!(Media::is_local_uri(&source).not());
964
965        // Test non-local encrypted source.
966        let source = MediaSource::Encrypted(encrypted_file(mxc_uri!("mxc://server.local/mlkjhg")));
967        assert!(Media::is_local_uri(&source).not());
968
969        // Test invalid MXC URI.
970        let source = MediaSource::Plain("https://server.local/nbvcxw".into());
971        assert!(Media::is_local_uri(&source).not());
972    }
973}