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