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.
1415//! 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
2829use ruma::time::{Duration, SystemTime};
30use serde::{Deserialize, Serialize};
3132/// 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")]
54pub max_cache_size: Option<u64>,
5556/// 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")]
72pub max_file_size: Option<u64>,
7374/// 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")]
82pub last_access_expiry: Option<Duration>,
8384/// 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")]
93pub cleanup_frequency: Option<Duration>,
94}
9596impl MediaRetentionPolicy {
97/// Create a [`MediaRetentionPolicy`] with the default values.
98pub fn new() -> Self {
99Self::default()
100 }
101102/// Create an empty [`MediaRetentionPolicy`].
103 ///
104 /// This means that all media will be cached and cleanups have no effect.
105pub fn empty() -> Self {
106Self {
107 max_cache_size: None,
108 max_file_size: None,
109 last_access_expiry: None,
110 cleanup_frequency: None,
111 }
112 }
113114/// Set the maximum authorized size of the overall media cache, in bytes.
115pub fn with_max_cache_size(mut self, size: Option<u64>) -> Self {
116self.max_cache_size = size;
117self
118}
119120/// Set the maximum authorized size of a single media content, in bytes.
121pub fn with_max_file_size(mut self, size: Option<u64>) -> Self {
122self.max_file_size = size;
123self
124}
125126/// Set the duration before which unaccessed media content is considered
127 /// expired.
128pub fn with_last_access_expiry(mut self, duration: Option<Duration>) -> Self {
129self.last_access_expiry = duration;
130self
131}
132133/// Set the duration between two automatic media cache cleanups.
134pub fn with_cleanup_frequency(mut self, duration: Option<Duration>) -> Self {
135self.cleanup_frequency = duration;
136self
137}
138139/// 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.
144pub fn has_limitations(&self) -> bool {
145self.max_cache_size.is_some()
146 || self.max_file_size.is_some()
147 || self.last_access_expiry.is_some()
148 }
149150/// 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.
156pub fn exceeds_max_cache_size(&self, size: u64) -> bool {
157self.max_cache_size.is_some_and(|max_size| size > max_size)
158 }
159160/// 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`.
164pub fn computed_max_file_size(&self) -> Option<u64> {
165match (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 }
172173/// 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.
179pub fn exceeds_max_file_size(&self, size: u64) -> bool {
180self.computed_max_file_size().is_some_and(|max_size| size > max_size)
181 }
182183/// 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.
191pub fn has_content_expired(
192&self,
193 current_time: SystemTime,
194 last_access_time: SystemTime,
195 ) -> bool {
196self.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 }
204205/// 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.
213pub fn should_clean_up(&self, current_time: SystemTime, last_cleanup_time: SystemTime) -> bool {
214self.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}
223224impl Default for MediaRetentionPolicy {
225fn default() -> Self {
226Self {
227// 400 MiB.
228max_cache_size: Some(400 * 1024 * 1024),
229// 20 MiB.
230max_file_size: Some(20 * 1024 * 1024),
231// 60 days.
232last_access_expiry: Some(Duration::from_secs(60 * 24 * 60 * 60)),
233// 1 day.
234cleanup_frequency: Some(Duration::from_secs(24 * 60 * 60)),
235 }
236 }
237}
238239#[cfg(test)]
240mod tests {
241use ruma::time::{Duration, SystemTime};
242243use super::MediaRetentionPolicy;
244245#[test]
246fn test_media_retention_policy_has_limitations() {
247let mut policy = MediaRetentionPolicy::empty();
248assert!(!policy.has_limitations());
249250 policy = policy.with_last_access_expiry(Some(Duration::from_secs(60)));
251assert!(policy.has_limitations());
252253 policy = policy.with_last_access_expiry(None);
254assert!(!policy.has_limitations());
255256 policy = policy.with_max_cache_size(Some(1_024));
257assert!(policy.has_limitations());
258259 policy = policy.with_max_cache_size(None);
260assert!(!policy.has_limitations());
261262 policy = policy.with_max_file_size(Some(1_024));
263assert!(policy.has_limitations());
264265 policy = policy.with_max_file_size(None);
266assert!(!policy.has_limitations());
267268// With default values.
269assert!(MediaRetentionPolicy::new().has_limitations());
270 }
271272#[test]
273fn test_media_retention_policy_max_cache_size() {
274let file_size = 2_048;
275276let mut policy = MediaRetentionPolicy::empty();
277assert!(!policy.exceeds_max_cache_size(file_size));
278assert_eq!(policy.computed_max_file_size(), None);
279assert!(!policy.exceeds_max_file_size(file_size));
280281 policy = policy.with_max_cache_size(Some(4_096));
282assert!(!policy.exceeds_max_cache_size(file_size));
283assert_eq!(policy.computed_max_file_size(), Some(4_096));
284assert!(!policy.exceeds_max_file_size(file_size));
285286 policy = policy.with_max_cache_size(Some(2_048));
287assert!(!policy.exceeds_max_cache_size(file_size));
288assert_eq!(policy.computed_max_file_size(), Some(2_048));
289assert!(!policy.exceeds_max_file_size(file_size));
290291 policy = policy.with_max_cache_size(Some(1_024));
292assert!(policy.exceeds_max_cache_size(file_size));
293assert_eq!(policy.computed_max_file_size(), Some(1_024));
294assert!(policy.exceeds_max_file_size(file_size));
295 }
296297#[test]
298fn test_media_retention_policy_max_file_size() {
299let file_size = 2_048;
300301let mut policy = MediaRetentionPolicy::empty();
302assert_eq!(policy.computed_max_file_size(), None);
303assert!(!policy.exceeds_max_file_size(file_size));
304305// With max_file_size only.
306policy = policy.with_max_file_size(Some(4_096));
307assert_eq!(policy.computed_max_file_size(), Some(4_096));
308assert!(!policy.exceeds_max_file_size(file_size));
309310 policy = policy.with_max_file_size(Some(2_048));
311assert_eq!(policy.computed_max_file_size(), Some(2_048));
312assert!(!policy.exceeds_max_file_size(file_size));
313314 policy = policy.with_max_file_size(Some(1_024));
315assert_eq!(policy.computed_max_file_size(), Some(1_024));
316assert!(policy.exceeds_max_file_size(file_size));
317318// With max_cache_size as well.
319policy = policy.with_max_cache_size(Some(2_048));
320assert_eq!(policy.computed_max_file_size(), Some(1_024));
321assert!(policy.exceeds_max_file_size(file_size));
322323 policy = policy.with_max_file_size(Some(2_048));
324assert_eq!(policy.computed_max_file_size(), Some(2_048));
325assert!(!policy.exceeds_max_file_size(file_size));
326327 policy = policy.with_max_file_size(Some(4_096));
328assert_eq!(policy.computed_max_file_size(), Some(2_048));
329assert!(!policy.exceeds_max_file_size(file_size));
330331 policy = policy.with_max_cache_size(Some(1_024));
332assert_eq!(policy.computed_max_file_size(), Some(1_024));
333assert!(policy.exceeds_max_file_size(file_size));
334 }
335336#[test]
337fn test_media_retention_policy_has_content_expired() {
338let epoch = SystemTime::UNIX_EPOCH;
339let last_access_time = epoch + Duration::from_secs(30);
340let epoch_plus_60 = epoch + Duration::from_secs(60);
341let epoch_plus_120 = epoch + Duration::from_secs(120);
342343let mut policy = MediaRetentionPolicy::empty();
344assert!(!policy.has_content_expired(epoch, last_access_time));
345assert!(!policy.has_content_expired(last_access_time, last_access_time));
346assert!(!policy.has_content_expired(epoch_plus_60, last_access_time));
347assert!(!policy.has_content_expired(epoch_plus_120, last_access_time));
348349 policy = policy.with_last_access_expiry(Some(Duration::from_secs(120)));
350assert!(!policy.has_content_expired(epoch, last_access_time));
351assert!(!policy.has_content_expired(last_access_time, last_access_time));
352assert!(!policy.has_content_expired(epoch_plus_60, last_access_time));
353assert!(!policy.has_content_expired(epoch_plus_120, last_access_time));
354355 policy = policy.with_last_access_expiry(Some(Duration::from_secs(60)));
356assert!(!policy.has_content_expired(epoch, last_access_time));
357assert!(!policy.has_content_expired(last_access_time, last_access_time));
358assert!(!policy.has_content_expired(epoch_plus_60, last_access_time));
359assert!(policy.has_content_expired(epoch_plus_120, last_access_time));
360361 policy = policy.with_last_access_expiry(Some(Duration::from_secs(30)));
362assert!(!policy.has_content_expired(epoch, last_access_time));
363assert!(!policy.has_content_expired(last_access_time, last_access_time));
364assert!(policy.has_content_expired(epoch_plus_60, last_access_time));
365assert!(policy.has_content_expired(epoch_plus_120, last_access_time));
366367 policy = policy.with_last_access_expiry(Some(Duration::from_secs(0)));
368assert!(!policy.has_content_expired(epoch, last_access_time));
369assert!(policy.has_content_expired(last_access_time, last_access_time));
370assert!(policy.has_content_expired(epoch_plus_60, last_access_time));
371assert!(policy.has_content_expired(epoch_plus_120, last_access_time));
372 }
373374#[test]
375fn test_media_retention_policy_cleanup_frequency() {
376let epoch = SystemTime::UNIX_EPOCH;
377let epoch_plus_60 = epoch + Duration::from_secs(60);
378let epoch_plus_120 = epoch + Duration::from_secs(120);
379380let mut policy = MediaRetentionPolicy::empty();
381assert!(!policy.should_clean_up(epoch_plus_60, epoch));
382assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_60));
383assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_120));
384385 policy = policy.with_cleanup_frequency(Some(Duration::from_secs(0)));
386assert!(policy.should_clean_up(epoch_plus_60, epoch));
387assert!(policy.should_clean_up(epoch_plus_60, epoch_plus_60));
388assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_120));
389390 policy = policy.with_cleanup_frequency(Some(Duration::from_secs(30)));
391assert!(policy.should_clean_up(epoch_plus_60, epoch));
392assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_60));
393assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_120));
394395 policy = policy.with_cleanup_frequency(Some(Duration::from_secs(60)));
396assert!(policy.should_clean_up(epoch_plus_60, epoch));
397assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_60));
398assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_120));
399400 policy = policy.with_cleanup_frequency(Some(Duration::from_secs(90)));
401assert!(!policy.should_clean_up(epoch_plus_60, epoch));
402assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_60));
403assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_120));
404 }
405}