atagen

matrix-rust-sdk

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