matrix_sdk_base/event_cache/store/media/
media_retention_policy.rs

1// Copyright 2025 Kévin Commaille
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Configuration to decide whether or not to keep media in the cache, allowing
16//! to do periodic cleanups to avoid to have the size of the media cache grow
17//! indefinitely.
18//!
19//! To proceed to a cleanup, first set the [`MediaRetentionPolicy`] to use with
20//! [`EventCacheStore::set_media_retention_policy()`]. Then call
21//! [`EventCacheStore::clean_up_media_cache()`].
22//!
23//! In the future, other settings will allow to run automatic periodic cleanup
24//! jobs.
25//!
26//! [`EventCacheStore::set_media_retention_policy()`]: crate::event_cache::store::EventCacheStore::set_media_retention_policy
27//! [`EventCacheStore::clean_up_media_cache()`]: crate::event_cache::store::EventCacheStore::clean_up_media_cache
28
29use ruma::time::{Duration, SystemTime};
30use serde::{Deserialize, Serialize};
31
32/// The retention policy for media content used by the [`EventCacheStore`].
33///
34/// [`EventCacheStore`]: crate::event_cache::store::EventCacheStore
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
36#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
37#[non_exhaustive]
38pub struct MediaRetentionPolicy {
39    /// The maximum authorized size of the overall media cache, in bytes.
40    ///
41    /// The cache size is defined as the sum of the sizes of all the (possibly
42    /// encrypted) media contents in the cache, excluding any metadata
43    /// associated with them.
44    ///
45    /// If this is set and the cache size is bigger than this value, the oldest
46    /// media contents in the cache will be removed during a cleanup until the
47    /// cache size is below this threshold.
48    ///
49    /// Note that it is possible for the cache size to temporarily exceed this
50    /// value between two cleanups.
51    ///
52    /// Defaults to 400 MiB.
53    #[serde(default, skip_serializing_if = "Option::is_none")]
54    pub max_cache_size: Option<u64>,
55
56    /// The maximum authorized size of a single media content, in bytes.
57    ///
58    /// The size of a media content is the size taken by the content in the
59    /// database, after it was possibly encrypted, so it might differ from the
60    /// initial size of the content.
61    ///
62    /// The maximum authorized size of a single media content is actually the
63    /// lowest value between `max_cache_size` and `max_file_size`.
64    ///
65    /// If it is set, media content bigger than the maximum size will not be
66    /// cached. If the maximum size changed after media content that exceeds the
67    /// new value was cached, the corresponding content will be removed
68    /// during a cleanup.
69    ///
70    /// Defaults to 20 MiB.
71    #[serde(default, skip_serializing_if = "Option::is_none")]
72    pub max_file_size: Option<u64>,
73
74    /// The duration after which unaccessed media content is considered
75    /// expired.
76    ///
77    /// If this is set, media content whose last access is older than this
78    /// duration will be removed from the media cache during a cleanup.
79    ///
80    /// Defaults to 60 days.
81    #[serde(default, skip_serializing_if = "Option::is_none")]
82    pub last_access_expiry: Option<Duration>,
83
84    /// The duration between two automatic media cache cleanups.
85    ///
86    /// If this is set, a cleanup will be triggered after the given duration
87    /// is elapsed, at the next call to the media cache API. If this is set to
88    /// zero, each call to the media cache API will trigger a cleanup. If this
89    /// is `None`, cleanups will only occur if they are triggered manually.
90    ///
91    /// Defaults to running cleanups daily.
92    #[serde(default, skip_serializing_if = "Option::is_none")]
93    pub cleanup_frequency: Option<Duration>,
94}
95
96impl MediaRetentionPolicy {
97    /// Create a [`MediaRetentionPolicy`] with the default values.
98    pub fn new() -> Self {
99        Self::default()
100    }
101
102    /// Create an empty [`MediaRetentionPolicy`].
103    ///
104    /// This means that all media will be cached and cleanups have no effect.
105    pub fn empty() -> Self {
106        Self {
107            max_cache_size: None,
108            max_file_size: None,
109            last_access_expiry: None,
110            cleanup_frequency: None,
111        }
112    }
113
114    /// Set the maximum authorized size of the overall media cache, in bytes.
115    pub fn with_max_cache_size(mut self, size: Option<u64>) -> Self {
116        self.max_cache_size = size;
117        self
118    }
119
120    /// Set the maximum authorized size of a single media content, in bytes.
121    pub fn with_max_file_size(mut self, size: Option<u64>) -> Self {
122        self.max_file_size = size;
123        self
124    }
125
126    /// Set the duration before which unaccessed media content is considered
127    /// expired.
128    pub fn with_last_access_expiry(mut self, duration: Option<Duration>) -> Self {
129        self.last_access_expiry = duration;
130        self
131    }
132
133    /// Set the duration between two automatic media cache cleanups.
134    pub fn with_cleanup_frequency(mut self, duration: Option<Duration>) -> Self {
135        self.cleanup_frequency = duration;
136        self
137    }
138
139    /// Whether this policy has limitations.
140    ///
141    /// If this policy has no limitations, a cleanup job would have no effect.
142    ///
143    /// Returns `true` if at least one limitation is set.
144    pub fn has_limitations(&self) -> bool {
145        self.max_cache_size.is_some()
146            || self.max_file_size.is_some()
147            || self.last_access_expiry.is_some()
148    }
149
150    /// Whether the given size exceeds the maximum authorized size of the media
151    /// cache.
152    ///
153    /// # Arguments
154    ///
155    /// * `size` - The overall size of the media cache to check, in bytes.
156    pub fn exceeds_max_cache_size(&self, size: u64) -> bool {
157        self.max_cache_size.is_some_and(|max_size| size > max_size)
158    }
159
160    /// The computed maximum authorized size of a single media content, in
161    /// bytes.
162    ///
163    /// This is the lowest value between `max_cache_size` and `max_file_size`.
164    pub fn computed_max_file_size(&self) -> Option<u64> {
165        match (self.max_cache_size, self.max_file_size) {
166            (None, None) => None,
167            (None, Some(size)) => Some(size),
168            (Some(size), None) => Some(size),
169            (Some(max_cache_size), Some(max_file_size)) => Some(max_cache_size.min(max_file_size)),
170        }
171    }
172
173    /// Whether the given size, in bytes, exceeds the computed maximum
174    /// authorized size of a single media content.
175    ///
176    /// # Arguments
177    ///
178    /// * `size` - The size of the media content to check, in bytes.
179    pub fn exceeds_max_file_size(&self, size: u64) -> bool {
180        self.computed_max_file_size().is_some_and(|max_size| size > max_size)
181    }
182
183    /// Whether a content whose last access was at the given time has expired.
184    ///
185    /// # Arguments
186    ///
187    /// * `current_time` - The current time.
188    ///
189    /// * `last_access_time` - The time when the media content to check was last
190    ///   accessed.
191    pub fn has_content_expired(
192        &self,
193        current_time: SystemTime,
194        last_access_time: SystemTime,
195    ) -> bool {
196        self.last_access_expiry.is_some_and(|max_duration| {
197            current_time
198                .duration_since(last_access_time)
199                // If this returns an error, the last access time is newer than the current time.
200                // This shouldn't happen but in this case the content cannot be expired.
201                .is_ok_and(|elapsed| elapsed >= max_duration)
202        })
203    }
204
205    /// Whether an automatic media cache cleanup should be triggered given the
206    /// time of the last cleanup.
207    ///
208    /// # Arguments
209    ///
210    /// * `current_time` - The current time.
211    ///
212    /// * `last_cleanup_time` - The time of the last media cache cleanup.
213    pub fn should_clean_up(&self, current_time: SystemTime, last_cleanup_time: SystemTime) -> bool {
214        self.cleanup_frequency.is_some_and(|max_duration| {
215            current_time
216                .duration_since(last_cleanup_time)
217                // If this returns an error, the last cleanup time is newer than the current time.
218                // This shouldn't happen but in this case no cleanup job is needed.
219                .is_ok_and(|elapsed| elapsed >= max_duration)
220        })
221    }
222}
223
224impl Default for MediaRetentionPolicy {
225    fn default() -> Self {
226        Self {
227            // 400 MiB.
228            max_cache_size: Some(400 * 1024 * 1024),
229            // 20 MiB.
230            max_file_size: Some(20 * 1024 * 1024),
231            // 60 days.
232            last_access_expiry: Some(Duration::from_secs(60 * 24 * 60 * 60)),
233            // 1 day.
234            cleanup_frequency: Some(Duration::from_secs(24 * 60 * 60)),
235        }
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use ruma::time::{Duration, SystemTime};
242
243    use super::MediaRetentionPolicy;
244
245    #[test]
246    fn test_media_retention_policy_has_limitations() {
247        let mut policy = MediaRetentionPolicy::empty();
248        assert!(!policy.has_limitations());
249
250        policy = policy.with_last_access_expiry(Some(Duration::from_secs(60)));
251        assert!(policy.has_limitations());
252
253        policy = policy.with_last_access_expiry(None);
254        assert!(!policy.has_limitations());
255
256        policy = policy.with_max_cache_size(Some(1_024));
257        assert!(policy.has_limitations());
258
259        policy = policy.with_max_cache_size(None);
260        assert!(!policy.has_limitations());
261
262        policy = policy.with_max_file_size(Some(1_024));
263        assert!(policy.has_limitations());
264
265        policy = policy.with_max_file_size(None);
266        assert!(!policy.has_limitations());
267
268        // With default values.
269        assert!(MediaRetentionPolicy::new().has_limitations());
270    }
271
272    #[test]
273    fn test_media_retention_policy_max_cache_size() {
274        let file_size = 2_048;
275
276        let mut policy = MediaRetentionPolicy::empty();
277        assert!(!policy.exceeds_max_cache_size(file_size));
278        assert_eq!(policy.computed_max_file_size(), None);
279        assert!(!policy.exceeds_max_file_size(file_size));
280
281        policy = policy.with_max_cache_size(Some(4_096));
282        assert!(!policy.exceeds_max_cache_size(file_size));
283        assert_eq!(policy.computed_max_file_size(), Some(4_096));
284        assert!(!policy.exceeds_max_file_size(file_size));
285
286        policy = policy.with_max_cache_size(Some(2_048));
287        assert!(!policy.exceeds_max_cache_size(file_size));
288        assert_eq!(policy.computed_max_file_size(), Some(2_048));
289        assert!(!policy.exceeds_max_file_size(file_size));
290
291        policy = policy.with_max_cache_size(Some(1_024));
292        assert!(policy.exceeds_max_cache_size(file_size));
293        assert_eq!(policy.computed_max_file_size(), Some(1_024));
294        assert!(policy.exceeds_max_file_size(file_size));
295    }
296
297    #[test]
298    fn test_media_retention_policy_max_file_size() {
299        let file_size = 2_048;
300
301        let mut policy = MediaRetentionPolicy::empty();
302        assert_eq!(policy.computed_max_file_size(), None);
303        assert!(!policy.exceeds_max_file_size(file_size));
304
305        // With max_file_size only.
306        policy = policy.with_max_file_size(Some(4_096));
307        assert_eq!(policy.computed_max_file_size(), Some(4_096));
308        assert!(!policy.exceeds_max_file_size(file_size));
309
310        policy = policy.with_max_file_size(Some(2_048));
311        assert_eq!(policy.computed_max_file_size(), Some(2_048));
312        assert!(!policy.exceeds_max_file_size(file_size));
313
314        policy = policy.with_max_file_size(Some(1_024));
315        assert_eq!(policy.computed_max_file_size(), Some(1_024));
316        assert!(policy.exceeds_max_file_size(file_size));
317
318        // With max_cache_size as well.
319        policy = policy.with_max_cache_size(Some(2_048));
320        assert_eq!(policy.computed_max_file_size(), Some(1_024));
321        assert!(policy.exceeds_max_file_size(file_size));
322
323        policy = policy.with_max_file_size(Some(2_048));
324        assert_eq!(policy.computed_max_file_size(), Some(2_048));
325        assert!(!policy.exceeds_max_file_size(file_size));
326
327        policy = policy.with_max_file_size(Some(4_096));
328        assert_eq!(policy.computed_max_file_size(), Some(2_048));
329        assert!(!policy.exceeds_max_file_size(file_size));
330
331        policy = policy.with_max_cache_size(Some(1_024));
332        assert_eq!(policy.computed_max_file_size(), Some(1_024));
333        assert!(policy.exceeds_max_file_size(file_size));
334    }
335
336    #[test]
337    fn test_media_retention_policy_has_content_expired() {
338        let epoch = SystemTime::UNIX_EPOCH;
339        let last_access_time = epoch + Duration::from_secs(30);
340        let epoch_plus_60 = epoch + Duration::from_secs(60);
341        let epoch_plus_120 = epoch + Duration::from_secs(120);
342
343        let mut policy = MediaRetentionPolicy::empty();
344        assert!(!policy.has_content_expired(epoch, last_access_time));
345        assert!(!policy.has_content_expired(last_access_time, last_access_time));
346        assert!(!policy.has_content_expired(epoch_plus_60, last_access_time));
347        assert!(!policy.has_content_expired(epoch_plus_120, last_access_time));
348
349        policy = policy.with_last_access_expiry(Some(Duration::from_secs(120)));
350        assert!(!policy.has_content_expired(epoch, last_access_time));
351        assert!(!policy.has_content_expired(last_access_time, last_access_time));
352        assert!(!policy.has_content_expired(epoch_plus_60, last_access_time));
353        assert!(!policy.has_content_expired(epoch_plus_120, last_access_time));
354
355        policy = policy.with_last_access_expiry(Some(Duration::from_secs(60)));
356        assert!(!policy.has_content_expired(epoch, last_access_time));
357        assert!(!policy.has_content_expired(last_access_time, last_access_time));
358        assert!(!policy.has_content_expired(epoch_plus_60, last_access_time));
359        assert!(policy.has_content_expired(epoch_plus_120, last_access_time));
360
361        policy = policy.with_last_access_expiry(Some(Duration::from_secs(30)));
362        assert!(!policy.has_content_expired(epoch, last_access_time));
363        assert!(!policy.has_content_expired(last_access_time, last_access_time));
364        assert!(policy.has_content_expired(epoch_plus_60, last_access_time));
365        assert!(policy.has_content_expired(epoch_plus_120, last_access_time));
366
367        policy = policy.with_last_access_expiry(Some(Duration::from_secs(0)));
368        assert!(!policy.has_content_expired(epoch, last_access_time));
369        assert!(policy.has_content_expired(last_access_time, last_access_time));
370        assert!(policy.has_content_expired(epoch_plus_60, last_access_time));
371        assert!(policy.has_content_expired(epoch_plus_120, last_access_time));
372    }
373
374    #[test]
375    fn test_media_retention_policy_cleanup_frequency() {
376        let epoch = SystemTime::UNIX_EPOCH;
377        let epoch_plus_60 = epoch + Duration::from_secs(60);
378        let epoch_plus_120 = epoch + Duration::from_secs(120);
379
380        let mut policy = MediaRetentionPolicy::empty();
381        assert!(!policy.should_clean_up(epoch_plus_60, epoch));
382        assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_60));
383        assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_120));
384
385        policy = policy.with_cleanup_frequency(Some(Duration::from_secs(0)));
386        assert!(policy.should_clean_up(epoch_plus_60, epoch));
387        assert!(policy.should_clean_up(epoch_plus_60, epoch_plus_60));
388        assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_120));
389
390        policy = policy.with_cleanup_frequency(Some(Duration::from_secs(30)));
391        assert!(policy.should_clean_up(epoch_plus_60, epoch));
392        assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_60));
393        assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_120));
394
395        policy = policy.with_cleanup_frequency(Some(Duration::from_secs(60)));
396        assert!(policy.should_clean_up(epoch_plus_60, epoch));
397        assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_60));
398        assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_120));
399
400        policy = policy.with_cleanup_frequency(Some(Duration::from_secs(90)));
401        assert!(!policy.should_clean_up(epoch_plus_60, epoch));
402        assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_60));
403        assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_120));
404    }
405}