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
282#[cfg(target_family = "wasm")]
283#[async_trait::async_trait(?Send)]
284impl MediaStoreInner for IndexeddbMediaStore {
285 type Error = IndexeddbMediaStoreError;
286
287 #[instrument(skip_all)]
288 async fn media_retention_policy_inner(
289 &self,
290 ) -> Result<Option<MediaRetentionPolicy>, IndexeddbMediaStoreError> {
291 let _timer = timer!("method");
292 self.transaction(&[MediaRetentionPolicy::OBJECT_STORE], TransactionMode::Readonly)?
293 .get_media_retention_policy()
294 .await
295 .map_err(Into::into)
296 }
297
298 #[instrument(skip_all)]
299 async fn set_media_retention_policy_inner(
300 &self,
301 policy: MediaRetentionPolicy,
302 ) -> Result<(), IndexeddbMediaStoreError> {
303 let _timer = timer!("method");
304
305 let transaction =
306 self.transaction(&[MediaRetentionPolicy::OBJECT_STORE], TransactionMode::Readwrite)?;
307 transaction.put_item(&policy).await?;
308 transaction.commit().await.map_err(Into::into)
309 }
310
311 #[instrument(skip_all)]
312 async fn add_media_content_inner(
313 &self,
314 request: &MediaRequestParameters,
315 content: Vec<u8>,
316 current_time: SystemTime,
317 policy: MediaRetentionPolicy,
318 ignore_policy: IgnoreMediaRetentionPolicy,
319 ) -> Result<(), IndexeddbMediaStoreError> {
320 let _timer = timer!("method");
321
322 let transaction = self.transaction(
323 &[MediaMetadata::OBJECT_STORE, MediaContent::OBJECT_STORE],
324 TransactionMode::Readwrite,
325 )?;
326
327 let media = Media {
328 request_parameters: request.clone(),
329 last_access: current_time.into(),
330 ignore_policy,
331 content,
332 };
333
334 transaction.put_media_if_policy_compliant(media, policy).await?;
335 transaction.commit().await.map_err(Into::into)
336 }
337
338 #[instrument(skip_all)]
339 async fn set_ignore_media_retention_policy_inner(
340 &self,
341 request: &MediaRequestParameters,
342 ignore_policy: IgnoreMediaRetentionPolicy,
343 ) -> Result<(), IndexeddbMediaStoreError> {
344 let _timer = timer!("method");
345
346 let transaction =
347 self.transaction(&[MediaMetadata::OBJECT_STORE], TransactionMode::Readwrite)?;
348 if let Some(mut metadata) = transaction.get_media_metadata_by_id(request).await?
349 && metadata.ignore_policy != ignore_policy
350 {
351 metadata.ignore_policy = ignore_policy;
352 transaction.put_media_metadata(&metadata).await?;
353 transaction.commit().await?;
354 }
355 Ok(())
356 }
357
358 #[instrument(skip_all)]
359 async fn get_media_content_inner(
360 &self,
361 request: &MediaRequestParameters,
362 current_time: SystemTime,
363 ) -> Result<Option<Vec<u8>>, IndexeddbMediaStoreError> {
364 let _timer = timer!("method");
365
366 let transaction = self.transaction(
367 &[MediaMetadata::OBJECT_STORE, MediaContent::OBJECT_STORE],
368 TransactionMode::Readwrite,
369 )?;
370 let media = transaction.access_media_by_id(request, current_time).await?;
371 transaction.commit().await?;
372 Ok(media.map(|m| m.content))
373 }
374
375 #[instrument(skip_all)]
376 async fn get_media_content_for_uri_inner(
377 &self,
378 uri: &MxcUri,
379 current_time: SystemTime,
380 ) -> Result<Option<Vec<u8>>, IndexeddbMediaStoreError> {
381 let _timer = timer!("method");
382
383 let transaction = self.transaction(
384 &[MediaMetadata::OBJECT_STORE, MediaContent::OBJECT_STORE],
385 TransactionMode::Readwrite,
386 )?;
387 let media = transaction.access_media_by_uri(uri, current_time).await?.pop();
388 transaction.commit().await?;
389 Ok(media.map(|m| m.content))
390 }
391
392 #[instrument(skip_all)]
393 async fn clean_inner(
394 &self,
395 policy: MediaRetentionPolicy,
396 current_time: SystemTime,
397 ) -> Result<(), IndexeddbMediaStoreError> {
398 let _timer = timer!("method");
399
400 if !policy.has_limitations() {
401 return Ok(());
402 }
403
404 let transaction = self.transaction(
405 &[
406 MediaMetadata::OBJECT_STORE,
407 MediaContent::OBJECT_STORE,
408 MediaCleanupTime::OBJECT_STORE,
409 ],
410 TransactionMode::Readwrite,
411 )?;
412
413 let ignore_policy = IgnoreMediaRetentionPolicy::No;
414 let current_time = UnixTime::from(current_time);
415
416 if let Some(max_file_size) = policy.computed_max_file_size() {
417 transaction
418 .delete_media_by_content_size_greater_than(ignore_policy, max_file_size as usize)
419 .await?;
420 }
421
422 if let Some(expiry) = policy.last_access_expiry {
423 transaction
424 .delete_media_by_last_access_earlier_than(ignore_policy, current_time - expiry)
425 .await?;
426 }
427
428 if let Some(max_cache_size) = policy.max_cache_size {
429 let cache_size = transaction
430 .get_cache_size(ignore_policy)
431 .await?
432 .ok_or(Self::Error::CacheSizeTooBig)?;
433 if cache_size > (max_cache_size as usize) {
434 let (_, upper_key) = transaction
435 .fold_media_metadata_keys_by_retention_while(
436 CursorDirection::Prev,
437 ignore_policy,
438 0usize,
439 |total, key| match total.checked_add(key.content_size()) {
440 None => None,
441 Some(total) if total > max_cache_size as usize => None,
442 Some(total) => Some(total),
443 },
444 )
445 .await?;
446 if let Some(upper_key) = upper_key {
447 transaction
448 .delete_media_by_retention_metadata_to(
449 upper_key.ignore_policy(),
450 upper_key.last_access(),
451 upper_key.content_size(),
452 )
453 .await?;
454 }
455 }
456 }
457
458 transaction.put_media_cleanup_time(current_time).await?;
459 transaction.commit().await.map_err(Into::into)
460 }
461
462 #[instrument(skip_all)]
463 async fn last_media_cleanup_time_inner(
464 &self,
465 ) -> Result<Option<SystemTime>, IndexeddbMediaStoreError> {
466 let _timer = timer!("method");
467 let time = self
468 .transaction(&[MediaCleanupTime::OBJECT_STORE], TransactionMode::Readonly)?
469 .get_media_cleanup_time()
470 .await?;
471 Ok(time.map(Into::into))
472 }
473}
474
475#[cfg(all(test, target_family = "wasm"))]
476mod tests {
477 use matrix_sdk_base::{
478 media::store::MediaStoreError, media_store_inner_integration_tests,
479 media_store_integration_tests, media_store_integration_tests_time,
480 };
481 use uuid::Uuid;
482
483 use crate::media_store::IndexeddbMediaStore;
484
485 mod unencrypted {
486 use super::*;
487
488 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
489
490 async fn get_media_store() -> Result<IndexeddbMediaStore, MediaStoreError> {
491 let name = format!("test-media-store-{}", Uuid::new_v4().as_hyphenated());
492 Ok(IndexeddbMediaStore::builder().database_name(name).build().await?)
493 }
494
495 #[cfg(target_family = "wasm")]
496 media_store_integration_tests!();
497
498 #[cfg(target_family = "wasm")]
499 media_store_integration_tests_time!();
500
501 #[cfg(target_family = "wasm")]
502 media_store_inner_integration_tests!(with_media_size_tests);
503 }
504
505 mod encrypted {
506 use std::sync::Arc;
507
508 use matrix_sdk_store_encryption::StoreCipher;
509
510 use super::*;
511
512 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
513
514 async fn get_media_store() -> Result<IndexeddbMediaStore, MediaStoreError> {
515 let name = format!("test-media-store-{}", Uuid::new_v4().as_hyphenated());
516 Ok(IndexeddbMediaStore::builder()
517 .database_name(name)
518 .store_cipher(Arc::new(StoreCipher::new().expect("store cipher")))
519 .build()
520 .await?)
521 }
522
523 #[cfg(target_family = "wasm")]
524 media_store_integration_tests!();
525
526 #[cfg(target_family = "wasm")]
527 media_store_integration_tests_time!();
528
529 #[cfg(target_family = "wasm")]
530 media_store_inner_integration_tests!();
531 }
532}