1#![allow(dead_code)]
19
20mod builder;
21mod error;
22mod migrations;
23mod serializer;
24mod transaction;
25mod types;
26use std::{collections::BTreeMap, rc::Rc, time::Duration};
27
28pub use builder::IndexeddbMediaStoreBuilder;
29pub use error::IndexeddbMediaStoreError;
30use indexed_db_futures::{
31 Build, cursor::CursorDirection, database::Database, transaction::TransactionMode,
32};
33#[cfg(target_family = "wasm")]
34use matrix_sdk_base::cross_process_lock::{
35 CrossProcessLockGeneration, FIRST_CROSS_PROCESS_LOCK_GENERATION,
36};
37use matrix_sdk_base::{
38 media::{
39 MediaRequestParameters,
40 store::{
41 IgnoreMediaRetentionPolicy, MediaRetentionPolicy, MediaService, MediaStore,
42 MediaStoreInner,
43 },
44 },
45 timer,
46};
47use ruma::{MilliSecondsSinceUnixEpoch, MxcUri, OwnedMxcUri, time::SystemTime};
48use tracing::instrument;
49
50use crate::{
51 media_store::{
52 transaction::IndexeddbMediaStoreTransaction,
53 types::{Lease, Media, MediaCleanupTime, MediaContent, MediaMetadata, UnixTime},
54 },
55 serializer::indexed_type::{IndexedTypeSerializer, traits::Indexed},
56 transaction::TransactionError,
57};
58
59#[derive(Debug, Clone)]
64pub struct IndexeddbMediaStore {
65 inner: Rc<Database>,
67 serializer: IndexedTypeSerializer,
69 media_service: MediaService,
72}
73
74impl IndexeddbMediaStore {
75 pub fn builder() -> IndexeddbMediaStoreBuilder {
78 IndexeddbMediaStoreBuilder::default()
79 }
80
81 pub fn transaction<'a>(
85 &'a self,
86 stores: &[&str],
87 mode: TransactionMode,
88 ) -> Result<IndexeddbMediaStoreTransaction<'a>, IndexeddbMediaStoreError> {
89 Ok(IndexeddbMediaStoreTransaction::new(
90 self.inner
91 .transaction(stores)
92 .with_mode(mode)
93 .build()
94 .map_err(TransactionError::from)?,
95 &self.serializer,
96 ))
97 }
98}
99
100#[cfg(target_family = "wasm")]
101#[async_trait::async_trait(?Send)]
102impl MediaStore for IndexeddbMediaStore {
103 type Error = IndexeddbMediaStoreError;
104
105 #[instrument(skip(self))]
106 async fn try_take_leased_lock(
107 &self,
108 lease_duration_ms: u32,
109 key: &str,
110 holder: &str,
111 ) -> Result<Option<CrossProcessLockGeneration>, IndexeddbMediaStoreError> {
112 let transaction = self.transaction(&[Lease::OBJECT_STORE], TransactionMode::Readwrite)?;
113
114 let now = Duration::from_millis(MilliSecondsSinceUnixEpoch::now().get().into());
115 let expiration = now + Duration::from_millis(lease_duration_ms.into());
116
117 let lease = match transaction.get_lease_by_id(key).await? {
118 Some(mut lease) => {
119 if lease.holder == holder {
120 lease.expiration = expiration;
122
123 Some(lease)
124 } else {
125 if lease.expiration < now {
127 lease.holder = holder.to_owned();
129 lease.expiration = expiration;
130 lease.generation += 1;
131
132 Some(lease)
133 } else {
134 None
136 }
137 }
138 }
139 None => {
140 let lease = Lease {
141 key: key.to_owned(),
142 holder: holder.to_owned(),
143 expiration,
144 generation: FIRST_CROSS_PROCESS_LOCK_GENERATION,
145 };
146
147 Some(lease)
148 }
149 };
150
151 Ok(if let Some(lease) = lease {
152 transaction.put_lease(&lease)?;
153 transaction.commit().await?;
154
155 Some(lease.generation)
156 } else {
157 None
158 })
159 }
160
161 #[instrument(skip_all)]
162 async fn add_media_content(
163 &self,
164 request: &MediaRequestParameters,
165 content: Vec<u8>,
166 ignore_policy: IgnoreMediaRetentionPolicy,
167 ) -> Result<(), IndexeddbMediaStoreError> {
168 let _timer = timer!("method");
169 self.media_service.add_media_content(self, request, content, ignore_policy).await
170 }
171
172 #[instrument(skip_all)]
173 async fn replace_media_key(
174 &self,
175 from: &MediaRequestParameters,
176 to: &MediaRequestParameters,
177 ) -> Result<(), IndexeddbMediaStoreError> {
178 let _timer = timer!("method");
179
180 let transaction =
181 self.transaction(&[MediaMetadata::OBJECT_STORE], TransactionMode::Readwrite)?;
182 if let Some(mut metadata) = transaction.get_media_metadata_by_id(from).await? {
183 transaction.delete_media_metadata_by_id(from).await?;
186 metadata.request_parameters = to.clone();
187 transaction.add_media_metadata(&metadata)?;
188 transaction.commit().await?;
189 }
190 Ok(())
191 }
192
193 #[instrument(skip_all)]
194 async fn get_media_content(
195 &self,
196 request: &MediaRequestParameters,
197 ) -> Result<Option<Vec<u8>>, IndexeddbMediaStoreError> {
198 let _timer = timer!("method");
199 self.media_service.get_media_content(self, request).await
200 }
201
202 #[instrument(skip_all)]
203 async fn remove_media_content(
204 &self,
205 request: &MediaRequestParameters,
206 ) -> Result<(), IndexeddbMediaStoreError> {
207 let _timer = timer!("method");
208
209 let transaction = self.transaction(
210 &[MediaMetadata::OBJECT_STORE, MediaContent::OBJECT_STORE],
211 TransactionMode::Readwrite,
212 )?;
213 transaction.delete_media_by_id(request).await?;
214 transaction.commit().await.map_err(Into::into)
215 }
216
217 #[instrument(skip(self))]
218 async fn remove_media_content_for_uri(
219 &self,
220 uri: &MxcUri,
221 ) -> Result<(), IndexeddbMediaStoreError> {
222 let _timer = timer!("method");
223
224 let transaction = self.transaction(
225 &[MediaMetadata::OBJECT_STORE, MediaContent::OBJECT_STORE],
226 TransactionMode::Readwrite,
227 )?;
228 transaction.delete_media_by_uri(uri).await?;
229 transaction.commit().await.map_err(Into::into)
230 }
231
232 #[instrument(skip_all)]
233 async fn set_media_retention_policy(
234 &self,
235 policy: MediaRetentionPolicy,
236 ) -> Result<(), IndexeddbMediaStoreError> {
237 let _timer = timer!("method");
238 self.media_service.set_media_retention_policy(self, policy).await
239 }
240
241 #[instrument(skip_all)]
242 fn media_retention_policy(&self) -> MediaRetentionPolicy {
243 let _timer = timer!("method");
244 self.media_service.media_retention_policy()
245 }
246
247 #[instrument(skip_all)]
248 async fn set_ignore_media_retention_policy(
249 &self,
250 request: &MediaRequestParameters,
251 ignore_policy: IgnoreMediaRetentionPolicy,
252 ) -> Result<(), IndexeddbMediaStoreError> {
253 let _timer = timer!("method");
254 self.media_service.set_ignore_media_retention_policy(self, request, ignore_policy).await
255 }
256
257 #[instrument(skip_all)]
258 async fn clean(&self) -> Result<(), IndexeddbMediaStoreError> {
259 let _timer = timer!("method");
260 self.media_service.clean(self).await
261 }
262
263 async fn optimize(&self) -> Result<(), Self::Error> {
264 Ok(())
265 }
266
267 async fn get_size(&self) -> Result<Option<usize>, Self::Error> {
268 self.transaction(&[MediaMetadata::OBJECT_STORE], TransactionMode::Readonly)?
269 .get_cache_size(IgnoreMediaRetentionPolicy::No)
270 .await
271 .map_err(Into::into)
272 }
273
274 #[instrument(skip_all)]
275 async fn media_sizes(
276 &self,
277 uris: &[OwnedMxcUri],
278 ) -> Result<BTreeMap<OwnedMxcUri, u64>, Self::Error> {
279 let _timer = timer!("method");
280
281 let transaction =
282 self.transaction(&[MediaMetadata::OBJECT_STORE], TransactionMode::Readonly)?;
283 let mut sizes = BTreeMap::new();
284
285 for uri in uris {
286 let metadata = transaction.get_media_metadata_by_uri(uri).await?;
287 if !metadata.is_empty() {
288 let size: usize = metadata.iter().map(|media| media.content_size).sum();
289 sizes.insert(uri.clone(), size as u64);
290 }
291 }
292
293 Ok(sizes)
294 }
295
296 async fn close(&self) -> Result<(), Self::Error> {
297 Ok(())
298 }
299
300 async fn reopen(&self) -> Result<(), Self::Error> {
301 Ok(())
302 }
303}
304
305#[cfg(target_family = "wasm")]
306#[async_trait::async_trait(?Send)]
307impl MediaStoreInner for IndexeddbMediaStore {
308 type Error = IndexeddbMediaStoreError;
309
310 #[instrument(skip_all)]
311 async fn media_retention_policy_inner(
312 &self,
313 ) -> Result<Option<MediaRetentionPolicy>, IndexeddbMediaStoreError> {
314 let _timer = timer!("method");
315 self.transaction(&[MediaRetentionPolicy::OBJECT_STORE], TransactionMode::Readonly)?
316 .get_media_retention_policy()
317 .await
318 .map_err(Into::into)
319 }
320
321 #[instrument(skip_all)]
322 async fn set_media_retention_policy_inner(
323 &self,
324 policy: MediaRetentionPolicy,
325 ) -> Result<(), IndexeddbMediaStoreError> {
326 let _timer = timer!("method");
327
328 let transaction =
329 self.transaction(&[MediaRetentionPolicy::OBJECT_STORE], TransactionMode::Readwrite)?;
330 transaction.put_item(&policy)?;
331 transaction.commit().await.map_err(Into::into)
332 }
333
334 #[instrument(skip_all)]
335 async fn add_media_content_inner(
336 &self,
337 request: &MediaRequestParameters,
338 content: Vec<u8>,
339 current_time: SystemTime,
340 policy: MediaRetentionPolicy,
341 ignore_policy: IgnoreMediaRetentionPolicy,
342 ) -> Result<(), IndexeddbMediaStoreError> {
343 let _timer = timer!("method");
344
345 let transaction = self.transaction(
346 &[MediaMetadata::OBJECT_STORE, MediaContent::OBJECT_STORE],
347 TransactionMode::Readwrite,
348 )?;
349
350 let media = Media {
351 request_parameters: request.clone(),
352 last_access: current_time.into(),
353 ignore_policy,
354 content,
355 };
356
357 transaction.put_media_if_policy_compliant(media, policy).await?;
358 transaction.commit().await.map_err(Into::into)
359 }
360
361 #[instrument(skip_all)]
362 async fn set_ignore_media_retention_policy_inner(
363 &self,
364 request: &MediaRequestParameters,
365 ignore_policy: IgnoreMediaRetentionPolicy,
366 ) -> Result<(), IndexeddbMediaStoreError> {
367 let _timer = timer!("method");
368
369 let transaction =
370 self.transaction(&[MediaMetadata::OBJECT_STORE], TransactionMode::Readwrite)?;
371 if let Some(mut metadata) = transaction.get_media_metadata_by_id(request).await?
372 && metadata.ignore_policy != ignore_policy
373 {
374 metadata.ignore_policy = ignore_policy;
375 transaction.put_media_metadata(&metadata)?;
376 transaction.commit().await?;
377 }
378 Ok(())
379 }
380
381 #[instrument(skip_all)]
382 async fn get_media_content_inner(
383 &self,
384 request: &MediaRequestParameters,
385 current_time: SystemTime,
386 ) -> Result<Option<Vec<u8>>, IndexeddbMediaStoreError> {
387 let _timer = timer!("method");
388
389 let transaction = self.transaction(
390 &[MediaMetadata::OBJECT_STORE, MediaContent::OBJECT_STORE],
391 TransactionMode::Readwrite,
392 )?;
393 let media = transaction.access_media_by_id(request, current_time).await?;
394 transaction.commit().await?;
395 Ok(media.map(|m| m.content))
396 }
397
398 #[instrument(skip_all)]
399 async fn clean_inner(
400 &self,
401 policy: MediaRetentionPolicy,
402 current_time: SystemTime,
403 ) -> Result<(), IndexeddbMediaStoreError> {
404 let _timer = timer!("method");
405
406 if !policy.has_limitations() {
407 return Ok(());
408 }
409
410 let transaction = self.transaction(
411 &[
412 MediaMetadata::OBJECT_STORE,
413 MediaContent::OBJECT_STORE,
414 MediaCleanupTime::OBJECT_STORE,
415 ],
416 TransactionMode::Readwrite,
417 )?;
418
419 let ignore_policy = IgnoreMediaRetentionPolicy::No;
420 let current_time = UnixTime::from(current_time);
421
422 if let Some(max_file_size) = policy.computed_max_file_size() {
423 transaction
424 .delete_media_by_content_size_greater_than(ignore_policy, max_file_size as usize)
425 .await?;
426 }
427
428 if let Some(expiry) = policy.last_access_expiry {
429 transaction
430 .delete_media_by_last_access_earlier_than(ignore_policy, current_time - expiry)
431 .await?;
432 }
433
434 if let Some(max_cache_size) = policy.max_cache_size {
435 let cache_size = transaction
436 .get_cache_size(ignore_policy)
437 .await?
438 .ok_or(Self::Error::CacheSizeTooBig)?;
439 if cache_size > (max_cache_size as usize) {
440 let (_, upper_key) = transaction
441 .fold_media_metadata_keys_by_retention_while(
442 CursorDirection::Prev,
443 ignore_policy,
444 0usize,
445 |total, key| match total.checked_add(key.content_size()) {
446 None => None,
447 Some(total) if total > max_cache_size as usize => None,
448 Some(total) => Some(total),
449 },
450 )
451 .await?;
452 if let Some(upper_key) = upper_key {
453 transaction
454 .delete_media_by_retention_metadata_to(
455 upper_key.ignore_policy(),
456 upper_key.last_access(),
457 upper_key.content_size(),
458 )
459 .await?;
460 }
461 }
462 }
463
464 transaction.put_media_cleanup_time(current_time)?;
465 transaction.commit().await.map_err(Into::into)
466 }
467
468 #[instrument(skip_all)]
469 async fn last_media_cleanup_time_inner(
470 &self,
471 ) -> Result<Option<SystemTime>, IndexeddbMediaStoreError> {
472 let _timer = timer!("method");
473 let time = self
474 .transaction(&[MediaCleanupTime::OBJECT_STORE], TransactionMode::Readonly)?
475 .get_media_cleanup_time()
476 .await?;
477 Ok(time.map(Into::into))
478 }
479}
480
481#[cfg(all(test, target_family = "wasm"))]
482mod tests {
483 use matrix_sdk_base::{
484 media::store::MediaStoreError, media_store_inner_integration_tests,
485 media_store_integration_tests, media_store_integration_tests_time,
486 };
487 use uuid::Uuid;
488
489 use crate::media_store::IndexeddbMediaStore;
490
491 mod unencrypted {
492 use super::*;
493
494 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
495
496 async fn get_media_store() -> Result<IndexeddbMediaStore, MediaStoreError> {
497 let name = format!("test-media-store-{}", Uuid::new_v4().as_hyphenated());
498 Ok(IndexeddbMediaStore::builder().database_name(name).build().await?)
499 }
500
501 #[cfg(target_family = "wasm")]
502 media_store_integration_tests!();
503
504 #[cfg(target_family = "wasm")]
505 media_store_integration_tests_time!();
506
507 #[cfg(target_family = "wasm")]
508 media_store_inner_integration_tests!(with_media_size_tests);
509 }
510
511 mod encrypted {
512 use std::sync::Arc;
513
514 use matrix_sdk_store_encryption::StoreCipher;
515
516 use super::*;
517
518 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
519
520 async fn get_media_store() -> Result<IndexeddbMediaStore, MediaStoreError> {
521 let name = format!("test-media-store-{}", Uuid::new_v4().as_hyphenated());
522 Ok(IndexeddbMediaStore::builder()
523 .database_name(name)
524 .store_cipher(Arc::new(StoreCipher::new().expect("store cipher")))
525 .build()
526 .await?)
527 }
528
529 #[cfg(target_family = "wasm")]
530 media_store_integration_tests!();
531
532 #[cfg(target_family = "wasm")]
533 media_store_integration_tests_time!();
534
535 #[cfg(target_family = "wasm")]
536 media_store_inner_integration_tests!();
537 }
538}