Skip to main content

matrix_sdk_indexeddb/media_store/
transaction.rs

1// Copyright 2025 The Matrix.org Foundation C.I.C.
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
15use std::ops::Deref;
16
17use indexed_db_futures::{cursor::CursorDirection, transaction as inner};
18use matrix_sdk_base::media::{
19    MediaRequestParameters,
20    store::{IgnoreMediaRetentionPolicy, MediaRetentionPolicy},
21};
22use ruma::MxcUri;
23use uuid::Uuid;
24
25use crate::{
26    media_store::{
27        serializer::indexed_types::{
28            IndexedCoreIdKey, IndexedLease, IndexedLeaseIdKey, IndexedMediaCleanupTime,
29            IndexedMediaContent, IndexedMediaContentIdKey, IndexedMediaMetadata,
30            IndexedMediaMetadataContentSizeKey, IndexedMediaMetadataIdKey,
31            IndexedMediaMetadataLastAccessKey, IndexedMediaMetadataRetentionKey,
32            IndexedMediaMetadataUriKey,
33        },
34        types::{Lease, Media, MediaCleanupTime, MediaContent, MediaMetadata, UnixTime},
35    },
36    serializer::indexed_type::{
37        IndexedTypeSerializer, range::IndexedKeyRange, traits::IndexedPrefixKeyComponentBounds,
38    },
39    transaction::{Transaction, TransactionError},
40};
41
42/// Represents an IndexedDB transaction, but provides a convenient interface for
43/// performing operations relevant to the IndexedDB implementation of
44/// [`MediaStore`](matrix_sdk_base::media::store::MediaStore).
45pub struct IndexeddbMediaStoreTransaction<'a> {
46    transaction: Transaction<'a>,
47}
48
49impl<'a> Deref for IndexeddbMediaStoreTransaction<'a> {
50    type Target = Transaction<'a>;
51
52    fn deref(&self) -> &Self::Target {
53        &self.transaction
54    }
55}
56
57impl<'a> IndexeddbMediaStoreTransaction<'a> {
58    pub fn new(transaction: inner::Transaction<'a>, serializer: &'a IndexedTypeSerializer) -> Self {
59        Self { transaction: Transaction::new(transaction, serializer) }
60    }
61
62    /// Returns the underlying IndexedDB transaction.
63    pub fn into_inner(self) -> Transaction<'a> {
64        self.transaction
65    }
66
67    /// Commit all operations tracked in this transaction to IndexedDB.
68    pub async fn commit(self) -> Result<(), TransactionError> {
69        self.transaction.commit().await
70    }
71
72    /// Query IndexedDB for the lease that matches the given key `id`. If more
73    /// than one lease is found, an error is returned.
74    pub async fn get_lease_by_id(&self, id: &str) -> Result<Option<Lease>, TransactionError> {
75        self.transaction.get_item_by_key_components::<Lease, IndexedLeaseIdKey>(id).await
76    }
77
78    /// Puts a lease into IndexedDB. If a media with the same key already
79    /// exists, it will be overwritten. When the item is successfully put, the
80    /// function returns the intermediary type [`IndexedLease`] in case
81    /// inspection is needed.
82    pub async fn put_lease(&self, lease: &Lease) -> Result<IndexedLease, TransactionError> {
83        self.transaction.put_item(lease).await
84    }
85
86    /// Query IndexedDB for the stored [`MediaRetentionPolicy`]
87    pub async fn get_media_retention_policy(
88        &self,
89    ) -> Result<Option<MediaRetentionPolicy>, TransactionError> {
90        self.transaction
91            .get_item_by_key_components::<MediaRetentionPolicy, IndexedCoreIdKey>(())
92            .await
93    }
94
95    /// Query IndexedDB for the stored [`MediaCleanupTime`]
96    pub async fn get_media_cleanup_time(
97        &self,
98    ) -> Result<Option<MediaCleanupTime>, TransactionError> {
99        self.transaction.get_item_by_key_components::<MediaCleanupTime, IndexedCoreIdKey>(()).await
100    }
101
102    /// Puts a media clean up time into IndexedDB. If one already exists, it
103    /// will be overwritten. When the item is successfully put, the
104    /// function returns the intermediary type [`IndexedMediaCLeanupTime`] in
105    /// case inspection is needed.
106    pub async fn put_media_cleanup_time(
107        &self,
108        time: impl Into<MediaCleanupTime>,
109    ) -> Result<IndexedMediaCleanupTime, TransactionError> {
110        let time: MediaCleanupTime = time.into();
111        self.transaction.put_item(&time).await
112    }
113
114    /// Query IndexedDB for [`MediaMetadata`] and [`MediaContent`] that matches
115    /// the given [`MediaRequestParameters`]. If an item is found, update
116    /// [`MediaMetadata::last_access`] using `current_time`. If more than one
117    /// item is found, an error is returned.
118    pub async fn access_media_by_id(
119        &self,
120        request_parameters: &MediaRequestParameters,
121        current_time: impl Into<UnixTime>,
122    ) -> Result<Option<Media>, TransactionError> {
123        if let Some(metadata) =
124            self.access_media_metadata_by_id(request_parameters, current_time).await?
125        {
126            let content = self
127                .get_media_content_by_id(metadata.content_id)
128                .await?
129                .ok_or(TransactionError::ItemNotFound)?;
130            Ok(Some(Media {
131                request_parameters: metadata.request_parameters,
132                last_access: metadata.last_access,
133                ignore_policy: metadata.ignore_policy,
134                content: content.data,
135            }))
136        } else {
137            Ok(None)
138        }
139    }
140
141    /// Query IndexedDB for [`MediaMetadata`] and [`MediaContent`] that matches
142    /// the given [`MxcUri`]. If an item is found, update
143    /// [`MediaMetadata::last_access`] using `current_time`. If more than
144    /// one item is found, an error is returned.
145    pub async fn access_media_by_uri(
146        &self,
147        uri: &MxcUri,
148        current_time: impl Into<UnixTime>,
149    ) -> Result<Vec<Media>, TransactionError> {
150        let mut medias = Vec::new();
151        for metadata in self.access_media_metadata_by_uri(uri, current_time).await? {
152            let content = self
153                .get_media_content_by_id(metadata.content_id)
154                .await?
155                .ok_or(TransactionError::ItemNotFound)?;
156            medias.push(Media {
157                request_parameters: metadata.request_parameters,
158                last_access: metadata.last_access,
159                ignore_policy: metadata.ignore_policy,
160                content: content.data,
161            });
162        }
163        Ok(medias)
164    }
165
166    /// Query IndexedDB for the size recorded in each
167    /// [`MediaMetadata::content_size`] which match
168    /// the given [`IgnoreMediaRetentionPolicy`]. Returns the sum of all sizes
169    /// or [`None`] if the size of the cache overflows [`usize::MAX`].
170    ///
171    /// Note that this operation is not constant, but rather iterates over all
172    /// keys and extracts the content size from each key.
173    pub async fn get_cache_size(
174        &self,
175        ignore_policy: IgnoreMediaRetentionPolicy,
176    ) -> Result<Option<usize>, TransactionError> {
177        Ok(self
178            .get_all_media_metadata_keys_by_content_size(ignore_policy)
179            .await?
180            .iter()
181            .try_fold(0usize, |size, key| size.checked_add(key.content_size())))
182    }
183
184    /// Adds [`MediaMetadata`] and [`MediaContent`] to IndexedDB if the size of
185    /// [`IndexedMediaContent::content`] does not exceed
186    /// [`MediaRetentionPolicy::max_file_size]. If an item with the same key
187    /// already exists, it will be overwritten.  When the item is
188    /// successfully put, the function returns the intermediary types
189    /// [`IndexedMediaMetadata`] and [`IndexedMediaContent`] in case inspection
190    /// is needed.
191    pub async fn put_media_if_policy_compliant(
192        &self,
193        media: Media,
194        policy: MediaRetentionPolicy,
195    ) -> Result<Option<(IndexedMediaMetadata, IndexedMediaContent)>, TransactionError> {
196        let content_id = match self.get_media_metadata_by_id(&media.request_parameters).await? {
197            Some(metadata) => metadata.content_id,
198            None => Uuid::new_v4(),
199        };
200        let content = MediaContent { content_id, data: media.content };
201        let option = if media.ignore_policy.is_yes() {
202            self.put_media_content(&content).await.map(Some)?
203        } else {
204            self.put_media_content_if_policy_compliant(&content, policy).await?
205        };
206        if let Some(indexed_content) = option {
207            let indexed_metadata = self
208                .put_media_metadata(&MediaMetadata {
209                    request_parameters: media.request_parameters,
210                    last_access: media.last_access,
211                    ignore_policy: media.ignore_policy,
212                    content_id,
213                    content_size: indexed_content.content.len(),
214                })
215                .await?;
216            Ok(Some((indexed_metadata, indexed_content)))
217        } else {
218            Ok(None)
219        }
220    }
221
222    /// Delete [`MediaMetadata`] and [`MediaContent`] that matches the given
223    /// [`MediaRequestParameters`] from IndexedDB
224    pub async fn delete_media_by_id(
225        &self,
226        request_parameters: &MediaRequestParameters,
227    ) -> Result<(), TransactionError> {
228        if let Some(metadata) = self.get_media_metadata_by_id(request_parameters).await? {
229            self.delete_media_content_by_id(metadata.content_id).await?;
230        }
231        self.delete_media_metadata_by_id(request_parameters).await
232    }
233
234    /// Delete [`MediaMetadata`] and [`MediaContent`] that matches the given
235    /// [`MxcUri`] from IndexedDB
236    pub async fn delete_media_by_uri(&self, uri: &MxcUri) -> Result<(), TransactionError> {
237        for metadata in self.get_media_metadata_by_uri(uri).await? {
238            self.delete_media_content_by_id(metadata.content_id).await?;
239        }
240        self.delete_media_metadata_by_uri(uri).await
241    }
242
243    /// Delete [`MediaMetadata`] and [`MediaContent`] that matches the given
244    /// [`IgnoreMediaRetentionPolicy`] and the given content size range from
245    /// IndexedDB
246    pub async fn delete_media_by_content_size(
247        &self,
248        ignore_policy: IgnoreMediaRetentionPolicy,
249        content_size: impl Into<IndexedKeyRange<usize>>,
250    ) -> Result<(), TransactionError> {
251        let range = content_size.into();
252        for key in self.get_media_metadata_keys_by_content_size(ignore_policy, range).await? {
253            self.delete_media_content_by_id(key.content_id()).await?;
254        }
255        self.delete_media_metadata_by_content_size(ignore_policy, range).await
256    }
257
258    /// Delete [`MediaMetadata`] and [`MediaContent`] that matches the given
259    /// [`IgnoreMediaRetentionPolicy`] and is strictly larger than the given
260    /// content size from IndexedDB
261    pub async fn delete_media_by_content_size_greater_than(
262        &self,
263        ignore_policy: IgnoreMediaRetentionPolicy,
264        content_size: usize,
265    ) -> Result<(), TransactionError> {
266        let (_, upper, _) =
267            IndexedMediaMetadataContentSizeKey::upper_key_components_with_prefix(ignore_policy);
268        self.delete_media_by_content_size(ignore_policy, (content_size + 1, upper)).await
269    }
270
271    /// Delete [`MediaMetadata`] and [`MediaContent`] that matches the given
272    /// [`IgnoreMediaRetentionPolicy`] and the given last access time range
273    /// from IndexedDB
274    pub async fn delete_media_by_last_access(
275        &self,
276        ignore_policy: IgnoreMediaRetentionPolicy,
277        last_access: impl Into<IndexedKeyRange<UnixTime>>,
278    ) -> Result<(), TransactionError> {
279        let range = last_access.into();
280        for key in self.get_media_metadata_keys_by_last_access(ignore_policy, range).await? {
281            self.delete_media_content_by_id(key.content_id()).await?;
282        }
283        self.delete_media_metadata_by_last_access(ignore_policy, range).await
284    }
285
286    /// Delete [`MediaMetadata`] and [`MediaContent`] that matches the given
287    /// [`IgnoreMediaRetentionPolicy`] and is earlier than the given last
288    /// access time from IndexedDB
289    pub async fn delete_media_by_last_access_earlier_than(
290        &self,
291        ignore_policy: IgnoreMediaRetentionPolicy,
292        time: UnixTime,
293    ) -> Result<(), TransactionError> {
294        let (_, lower, _) =
295            IndexedMediaMetadataLastAccessKey::lower_key_components_with_prefix(ignore_policy);
296        self.delete_media_by_last_access(ignore_policy, (lower, time)).await
297    }
298
299    /// Delete [`MediaMetadata`] and [`MediaContent`] that matches the given
300    /// [`IgnoreMediaRetentionPolicy`] and the given last access time and
301    /// content size range from IndexedDB
302    pub async fn delete_media_by_retention_metadata(
303        &self,
304        ignore_policy: IgnoreMediaRetentionPolicy,
305        range: impl Into<IndexedKeyRange<(UnixTime, usize)>>,
306    ) -> Result<(), TransactionError> {
307        let range = range.into();
308        for key in self.get_media_metadata_keys_by_retention(ignore_policy, range).await? {
309            self.delete_media_content_by_id(key.content_id()).await?;
310        }
311        self.delete_media_metadata_by_retention(ignore_policy, range).await
312    }
313
314    /// Delete [`MediaMetadata`] and [`MediaContent`] that matches the given
315    /// [`IgnoreMediaRetentionPolicy`] and is sorted before the given last
316    /// access time and content size from IndexedDB
317    pub async fn delete_media_by_retention_metadata_to(
318        &self,
319        ignore_policy: IgnoreMediaRetentionPolicy,
320        last_access: UnixTime,
321        content_size: usize,
322    ) -> Result<(), TransactionError> {
323        let (_, lower_last_access, lower_content_size, _) =
324            IndexedMediaMetadataRetentionKey::lower_key_components_with_prefix(ignore_policy);
325        let lower = (lower_last_access, lower_content_size);
326        self.delete_media_by_retention_metadata(ignore_policy, (lower, (last_access, content_size)))
327            .await
328    }
329
330    /// Query IndexedDB for [`MediaMetadata`] that matches the given
331    /// [`MediaRequestParameters`]. If more than one item is found, an error
332    /// is returned.
333    pub async fn get_media_metadata_by_id(
334        &self,
335        request_parameters: &MediaRequestParameters,
336    ) -> Result<Option<MediaMetadata>, TransactionError> {
337        self.get_item_by_key_components::<MediaMetadata, IndexedMediaMetadataIdKey>(
338            request_parameters,
339        )
340        .await
341    }
342
343    /// Query IndexedDB for [`MediaMetadata`] that matches the given
344    /// [`MediaRequestParameters`]. If an item is found, update
345    /// [`MediaMetadata::last_access`] using `current_time`. If more than one
346    /// item is found, an error is returned.
347    pub async fn access_media_metadata_by_id(
348        &self,
349        request_parameters: &MediaRequestParameters,
350        current_time: impl Into<UnixTime>,
351    ) -> Result<Option<MediaMetadata>, TransactionError> {
352        if let Some(mut media_metadata) = self.get_media_metadata_by_id(request_parameters).await? {
353            let last_access = media_metadata.last_access;
354            media_metadata.last_access = current_time.into();
355            self.put_item(&media_metadata).await?;
356            media_metadata.last_access = last_access;
357            Ok(Some(media_metadata))
358        } else {
359            Ok(None)
360        }
361    }
362
363    /// Query IndexedDB for [`MediaMetadata`] that match the given [`MxcUri`].
364    pub async fn get_media_metadata_by_uri(
365        &self,
366        uri: &MxcUri,
367    ) -> Result<Vec<MediaMetadata>, TransactionError> {
368        self.get_items_by_key_components::<MediaMetadata, IndexedMediaMetadataUriKey>(uri).await
369    }
370
371    /// Query IndexedDB for [`MediaMetadata`] that matches the given
372    /// [`MxcUri`]. If an item is found, update [`MediaMetadata::last_access`]
373    /// using `current_time`. If more than one item is found, an error
374    /// is returned.
375    pub async fn access_media_metadata_by_uri(
376        &self,
377        uri: &MxcUri,
378        current_time: impl Into<UnixTime>,
379    ) -> Result<Vec<MediaMetadata>, TransactionError> {
380        let current_time = current_time.into();
381        let mut media_metadatas = Vec::new();
382        for mut media_metadata in self.get_media_metadata_by_uri(uri).await? {
383            let last_access = media_metadata.last_access;
384            media_metadata.last_access = current_time;
385            self.put_item(&media_metadata).await?;
386            media_metadata.last_access = last_access;
387            media_metadatas.push(media_metadata);
388        }
389        Ok(media_metadatas)
390    }
391
392    /// Query IndexedDB for [content size](IndexedMediaMetadataContentSizeKey)
393    /// keys whose associated [`MediaMetadata`] matches the given
394    /// [`IgnoreMediaRetentionPolicy`] and content size range.
395    pub async fn get_media_metadata_keys_by_content_size(
396        &self,
397        ignore_policy: IgnoreMediaRetentionPolicy,
398        content_size: impl Into<IndexedKeyRange<usize>>,
399    ) -> Result<Vec<IndexedMediaMetadataContentSizeKey>, TransactionError> {
400        let range = Into::<IndexedKeyRange<usize>>::into(content_size)
401            .map(|last_access| (ignore_policy, last_access))
402            .into_prefix(self.serializer().inner());
403        self.get_keys::<MediaMetadata, IndexedMediaMetadataContentSizeKey>(range).await
404    }
405
406    /// Query IndexedDB for all [content
407    /// size](IndexedMediaMetadataContentSizeKey) keys whose associated
408    /// [`MediaMetadata`] matches the given [`IgnoreMediaRetentionPolicy`].
409    pub async fn get_all_media_metadata_keys_by_content_size(
410        &self,
411        ignore_policy: IgnoreMediaRetentionPolicy,
412    ) -> Result<Vec<IndexedMediaMetadataContentSizeKey>, TransactionError> {
413        let (_, lower, _) =
414            IndexedMediaMetadataContentSizeKey::lower_key_components_with_prefix(ignore_policy);
415        let (_, upper, _) =
416            IndexedMediaMetadataContentSizeKey::upper_key_components_with_prefix(ignore_policy);
417        self.get_media_metadata_keys_by_content_size(ignore_policy, (lower, upper)).await
418    }
419
420    /// Query IndexedDB for [last access](IndexedMediaMetadataLastAccessKey)
421    /// keys whose associated [`MediaMetadata`] matches the given
422    /// [`IgnoreMediaRetentionPolicy`] and last access time range.
423    pub async fn get_media_metadata_keys_by_last_access(
424        &self,
425        ignore_policy: IgnoreMediaRetentionPolicy,
426        last_access: impl Into<IndexedKeyRange<UnixTime>>,
427    ) -> Result<Vec<IndexedMediaMetadataLastAccessKey>, TransactionError> {
428        let range = Into::<IndexedKeyRange<UnixTime>>::into(last_access)
429            .map(|last_access| (ignore_policy, last_access))
430            .into_prefix(self.serializer().inner());
431        self.get_keys::<MediaMetadata, IndexedMediaMetadataLastAccessKey>(range).await
432    }
433
434    /// Query IndexedDB for [retention](IndexedMediaMetadataRetentionKey)
435    /// keys whose associated [`MediaMetadata`] matches the given
436    /// [`IgnoreMediaRetentionPolicy`] and last access time and content size
437    /// range.
438    pub async fn get_media_metadata_keys_by_retention(
439        &self,
440        ignore_policy: IgnoreMediaRetentionPolicy,
441        range: impl Into<IndexedKeyRange<(UnixTime, usize)>>,
442    ) -> Result<Vec<IndexedMediaMetadataRetentionKey>, TransactionError> {
443        let range = Into::<IndexedKeyRange<(UnixTime, usize)>>::into(range)
444            .map(|(last_access, content_size)| (ignore_policy, last_access, content_size))
445            .into_prefix(self.serializer().inner());
446        self.get_keys::<MediaMetadata, IndexedMediaMetadataRetentionKey>(range).await
447    }
448
449    /// Query IndexedDB for [retention metadata][1] keys that match the given
450    /// key range. Iterate over the keys in the given
451    /// [`direction`](CursorDirection) using a cursor and fold them into an
452    /// accumulator while the given function `f` returns [`Some`].
453    ///
454    /// This function returns the final value of the accumulator and the key, if
455    /// any, which caused the fold to short circuit.
456    ///
457    /// Note that the use of cursor means that keys are read lazily from
458    /// IndexedDB.
459    ///
460    /// [1]: crate::media_store::serializer::indexed_types::IndexedMediaMetadataRetentionKey
461    pub async fn fold_media_metadata_keys_by_retention_while<Acc, F>(
462        &self,
463        direction: CursorDirection,
464        ignore_policy: IgnoreMediaRetentionPolicy,
465        init: Acc,
466        f: F,
467    ) -> Result<(Acc, Option<IndexedMediaMetadataRetentionKey>), TransactionError>
468    where
469        F: FnMut(&Acc, &IndexedMediaMetadataRetentionKey) -> Option<Acc>,
470    {
471        self.fold_keys_while::<MediaMetadata, IndexedMediaMetadataRetentionKey, Acc, F>(
472            direction,
473            IndexedKeyRange::all_with_prefix(ignore_policy, self.serializer().inner()),
474            init,
475            f,
476        )
477        .await
478    }
479
480    /// Adds [`MediaMetadata`] to IndexedDB. If an item with the same key
481    /// already exists, it will be rejected. When the item is successfully
482    /// added, the function returns the intermediary type
483    /// [`IndexedMediaMetadata`] in case inspection is needed.
484    pub async fn add_media_metadata(
485        &self,
486        media_metadata: &MediaMetadata,
487    ) -> Result<IndexedMediaMetadata, TransactionError> {
488        self.add_item(media_metadata).await
489    }
490
491    /// Puts [`MediaMetadata`] in IndexedDB object. If an item with the same key
492    /// already exists, it will be overwritten. When the item is successfully
493    /// put, the function returns the intermediary type
494    /// [`IndexedMediaMetadata`] in case inspection is needed.
495    pub async fn put_media_metadata(
496        &self,
497        media_metadata: &MediaMetadata,
498    ) -> Result<IndexedMediaMetadata, TransactionError> {
499        self.put_item(media_metadata).await
500    }
501
502    /// Delete [`MediaMetadata`] that match the given [`MediaRequestParameters`]
503    /// from IndexedDB
504    pub async fn delete_media_metadata_by_id(
505        &self,
506        request_parameters: &MediaRequestParameters,
507    ) -> Result<(), TransactionError> {
508        self.delete_item_by_key::<MediaMetadata, IndexedMediaMetadataIdKey>(request_parameters)
509            .await
510    }
511
512    /// Delete [`MediaMetadata`] that matches the given [`MxcUri`]
513    /// from IndexedDB
514    pub async fn delete_media_metadata_by_uri(
515        &self,
516        source: &MxcUri,
517    ) -> Result<(), TransactionError> {
518        self.delete_item_by_key::<MediaMetadata, IndexedMediaMetadataUriKey>(source).await
519    }
520
521    /// Delete [`MediaMetadata`] that matches the given
522    /// [`IgnoreMediaRetentionPolicy`] and the given content size range from
523    /// IndexedDB
524    pub async fn delete_media_metadata_by_content_size(
525        &self,
526        ignore_policy: IgnoreMediaRetentionPolicy,
527        content_size: impl Into<IndexedKeyRange<usize>>,
528    ) -> Result<(), TransactionError> {
529        let range = Into::<IndexedKeyRange<usize>>::into(content_size)
530            .map(|size| (ignore_policy, size))
531            .into_prefix(self.serializer().inner());
532        self.delete_items_by_key::<MediaMetadata, IndexedMediaMetadataContentSizeKey>(range).await
533    }
534
535    /// Delete [`MediaMetadata`] that matches the given
536    /// [`IgnoreMediaRetentionPolicy`] and is strictly larger than the given
537    /// content size from IndexedDB
538    pub async fn delete_media_metadata_by_content_size_greater_than(
539        &self,
540        ignore_policy: IgnoreMediaRetentionPolicy,
541        content_size: usize,
542    ) -> Result<(), TransactionError> {
543        let (_, upper, _) =
544            IndexedMediaMetadataContentSizeKey::upper_key_components_with_prefix(ignore_policy);
545        self.delete_media_metadata_by_content_size(ignore_policy, (content_size + 1, upper)).await
546    }
547
548    /// Delete [`MediaMetadata`] that matches the given
549    /// [`IgnoreMediaRetentionPolicy`] and the given last access time range
550    /// from IndexedDB
551    pub async fn delete_media_metadata_by_last_access(
552        &self,
553        ignore_policy: IgnoreMediaRetentionPolicy,
554        last_access: impl Into<IndexedKeyRange<UnixTime>>,
555    ) -> Result<(), TransactionError> {
556        let range = Into::<IndexedKeyRange<UnixTime>>::into(last_access)
557            .map(|last_access| (ignore_policy, last_access))
558            .into_prefix(self.serializer().inner());
559        self.delete_items_by_key::<MediaMetadata, IndexedMediaMetadataLastAccessKey>(range).await
560    }
561
562    /// Delete [`MediaMetadata`] that matches the given
563    /// [`IgnoreMediaRetentionPolicy`] and is earlier than the given last
564    /// access time from IndexedDB
565    pub async fn delete_media_metadata_by_last_access_earlier_than(
566        &self,
567        ignore_policy: IgnoreMediaRetentionPolicy,
568        time: UnixTime,
569    ) -> Result<(), TransactionError> {
570        let (_, lower, _) =
571            IndexedMediaMetadataLastAccessKey::lower_key_components_with_prefix(ignore_policy);
572        self.delete_media_metadata_by_last_access(ignore_policy, (lower, time)).await
573    }
574
575    /// Delete [`MediaMetadata`] that matches the given
576    /// [`IgnoreMediaRetentionPolicy`] and the given last access time and
577    /// content size range from IndexedDB
578    pub async fn delete_media_metadata_by_retention(
579        &self,
580        ignore_policy: IgnoreMediaRetentionPolicy,
581        range: impl Into<IndexedKeyRange<(UnixTime, usize)>>,
582    ) -> Result<(), TransactionError> {
583        let range = Into::<IndexedKeyRange<(UnixTime, usize)>>::into(range)
584            .map(|(last_access, content_size)| (ignore_policy, last_access, content_size))
585            .into_prefix(self.serializer().inner());
586        self.delete_items_by_key::<MediaMetadata, IndexedMediaMetadataRetentionKey>(range).await
587    }
588
589    /// Delete [`MediaMetadata`] that matches the given
590    /// [`IgnoreMediaRetentionPolicy`] and is sorted before the given last
591    /// access time and content size from IndexedDB
592    pub async fn delete_media_metadata_by_retention_to(
593        &self,
594        ignore_policy: IgnoreMediaRetentionPolicy,
595        last_access: UnixTime,
596        content_size: usize,
597    ) -> Result<(), TransactionError> {
598        let (_, lower_last_access, lower_content_size, _) =
599            IndexedMediaMetadataRetentionKey::lower_key_components_with_prefix(ignore_policy);
600        let lower = (lower_last_access, lower_content_size);
601        self.delete_media_metadata_by_retention(ignore_policy, (lower, (last_access, content_size)))
602            .await
603    }
604
605    /// Query IndexedDB for [`Media`] that matches the given
606    /// identifier. If more than one item is found, an error
607    /// is returned.
608    pub async fn get_media_content_by_id(
609        &self,
610        id: Uuid,
611    ) -> Result<Option<MediaContent>, TransactionError> {
612        self.get_item_by_key_components::<MediaContent, IndexedMediaContentIdKey>(id).await
613    }
614
615    /// Adds [`MediaContent`] to IndexedDB. If an item with the same key already
616    /// exists, it will be rejected. When the item is successfully added, the
617    /// function returns the intermediary type [`IndexedMediaContent`] in case
618    /// inspection is needed.
619    pub async fn add_media_content(
620        &self,
621        content: &MediaContent,
622    ) -> Result<IndexedMediaContent, TransactionError> {
623        self.add_item(content).await
624    }
625
626    /// Puts [`MediaContent`] in IndexedDB object. If an item with the same key
627    /// already exists, it will be overwritten. When the item is successfully
628    /// put, the function returns the intermediary type
629    /// [`IndexedMediaContent`] in case inspection is needed.
630    pub async fn put_media_content(
631        &self,
632        content: &MediaContent,
633    ) -> Result<IndexedMediaContent, TransactionError> {
634        self.put_item(content).await
635    }
636
637    /// Adds [`MediaContent`] to IndexedDB if the size of
638    /// [`IndexedMediaContent::content`] does not exceed
639    /// [`MediaRetentionPolicy::max_file_size]. If an item with the same key
640    /// already exists, it will be overwritten. When the item is successfully
641    /// put, the function returns the intermediary type
642    /// [`IndexedMediaContent`] in case inspection is needed.
643    pub async fn put_media_content_if_policy_compliant(
644        &self,
645        media: &MediaContent,
646        policy: MediaRetentionPolicy,
647    ) -> Result<Option<IndexedMediaContent>, TransactionError> {
648        self.put_item_if(media, |indexed| {
649            !policy.exceeds_max_file_size(indexed.content.len() as u64)
650        })
651        .await
652    }
653
654    /// Delete [`MediaContent`] that match the given identifier from IndexedDB
655    pub async fn delete_media_content_by_id(&self, id: Uuid) -> Result<(), TransactionError> {
656        self.delete_item_by_key::<MediaContent, IndexedMediaContentIdKey>(id).await
657    }
658}