matrix_sdk_indexeddb/media_store/
mod.rs1#![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 remove_media_content_for_uri(
220 &self,
221 uri: &MxcUri,
222 ) -> Result<(), IndexeddbMediaStoreError> {
223 let _timer = timer!("method");
224
225 let transaction = self.transaction(
226 &[MediaMetadata::OBJECT_STORE, MediaContent::OBJECT_STORE],
227 TransactionMode::Readwrite,
228 )?;
229 transaction.delete_media_by_uri(uri).await?;
230 transaction.commit().await.map_err(Into::into)
231 }
232
233 #[instrument(skip_all)]
234 async fn set_media_retention_policy(
235 &self,
236 policy: MediaRetentionPolicy,
237 ) -> Result<(), IndexeddbMediaStoreError> {
238 let _timer = timer!("method");
239 self.media_service.set_media_retention_policy(self, policy).await
240 }
241
242 #[instrument(skip_all)]
243 fn media_retention_policy(&self) -> MediaRetentionPolicy {
244 let _timer = timer!("method");
245 self.media_service.media_retention_policy()
246 }
247
248 #[instrument(skip_all)]
249 async fn set_ignore_media_retention_policy(
250 &self,
251 request: &MediaRequestParameters,
252 ignore_policy: IgnoreMediaRetentionPolicy,
253 ) -> Result<(), IndexeddbMediaStoreError> {
254 let _timer = timer!("method");
255 self.media_service.set_ignore_media_retention_policy(self, request, ignore_policy).await
256 }
257
258 #[instrument(skip_all)]
259 async fn clean(&self) -> Result<(), IndexeddbMediaStoreError> {
260 let _timer = timer!("method");
261 self.media_service.clean(self).await
262 }
263
264 async fn optimize(&self) -> Result<(), Self::Error> {
265 Ok(())
266 }
267
268 async fn get_size(&self) -> Result<Option<usize>, Self::Error> {
269 Ok(None)
270 }
271
272 async fn close(&self) -> Result<(), Self::Error> {
273 Ok(())
274 }
275
276 async fn reopen(&self) -> Result<(), Self::Error> {
277 Ok(())
278 }
279}
280
281#[cfg(target_family = "wasm")]
282#[async_trait::async_trait(?Send)]
283impl MediaStoreInner for IndexeddbMediaStore {
284 type Error = IndexeddbMediaStoreError;
285
286 #[instrument(skip_all)]
287 async fn media_retention_policy_inner(
288 &self,
289 ) -> Result<Option<MediaRetentionPolicy>, IndexeddbMediaStoreError> {
290 let _timer = timer!("method");
291 self.transaction(&[MediaRetentionPolicy::OBJECT_STORE], TransactionMode::Readonly)?
292 .get_media_retention_policy()
293 .await
294 .map_err(Into::into)
295 }
296
297 #[instrument(skip_all)]
298 async fn set_media_retention_policy_inner(
299 &self,
300 policy: MediaRetentionPolicy,
301 ) -> Result<(), IndexeddbMediaStoreError> {
302 let _timer = timer!("method");
303
304 let transaction =
305 self.transaction(&[MediaRetentionPolicy::OBJECT_STORE], TransactionMode::Readwrite)?;
306 transaction.put_item(&policy).await?;
307 transaction.commit().await.map_err(Into::into)
308 }
309
310 #[instrument(skip_all)]
311 async fn add_media_content_inner(
312 &self,
313 request: &MediaRequestParameters,
314 content: Vec<u8>,
315 current_time: SystemTime,
316 policy: MediaRetentionPolicy,
317 ignore_policy: IgnoreMediaRetentionPolicy,
318 ) -> Result<(), IndexeddbMediaStoreError> {
319 let _timer = timer!("method");
320
321 let transaction = self.transaction(
322 &[MediaMetadata::OBJECT_STORE, MediaContent::OBJECT_STORE],
323 TransactionMode::Readwrite,
324 )?;
325
326 let media = Media {
327 request_parameters: request.clone(),
328 last_access: current_time.into(),
329 ignore_policy,
330 content,
331 };
332
333 transaction.put_media_if_policy_compliant(media, policy).await?;
334 transaction.commit().await.map_err(Into::into)
335 }
336
337 #[instrument(skip_all)]
338 async fn set_ignore_media_retention_policy_inner(
339 &self,
340 request: &MediaRequestParameters,
341 ignore_policy: IgnoreMediaRetentionPolicy,
342 ) -> Result<(), IndexeddbMediaStoreError> {
343 let _timer = timer!("method");
344
345 let transaction =
346 self.transaction(&[MediaMetadata::OBJECT_STORE], TransactionMode::Readwrite)?;
347 if let Some(mut metadata) = transaction.get_media_metadata_by_id(request).await?
348 && metadata.ignore_policy != ignore_policy
349 {
350 metadata.ignore_policy = ignore_policy;
351 transaction.put_media_metadata(&metadata).await?;
352 transaction.commit().await?;
353 }
354 Ok(())
355 }
356
357 #[instrument(skip_all)]
358 async fn get_media_content_inner(
359 &self,
360 request: &MediaRequestParameters,
361 current_time: SystemTime,
362 ) -> Result<Option<Vec<u8>>, IndexeddbMediaStoreError> {
363 let _timer = timer!("method");
364
365 let transaction = self.transaction(
366 &[MediaMetadata::OBJECT_STORE, MediaContent::OBJECT_STORE],
367 TransactionMode::Readwrite,
368 )?;
369 let media = transaction.access_media_by_id(request, current_time).await?;
370 transaction.commit().await?;
371 Ok(media.map(|m| m.content))
372 }
373
374 #[instrument(skip_all)]
375 async fn clean_inner(
376 &self,
377 policy: MediaRetentionPolicy,
378 current_time: SystemTime,
379 ) -> Result<(), IndexeddbMediaStoreError> {
380 let _timer = timer!("method");
381
382 if !policy.has_limitations() {
383 return Ok(());
384 }
385
386 let transaction = self.transaction(
387 &[
388 MediaMetadata::OBJECT_STORE,
389 MediaContent::OBJECT_STORE,
390 MediaCleanupTime::OBJECT_STORE,
391 ],
392 TransactionMode::Readwrite,
393 )?;
394
395 let ignore_policy = IgnoreMediaRetentionPolicy::No;
396 let current_time = UnixTime::from(current_time);
397
398 if let Some(max_file_size) = policy.computed_max_file_size() {
399 transaction
400 .delete_media_by_content_size_greater_than(ignore_policy, max_file_size as usize)
401 .await?;
402 }
403
404 if let Some(expiry) = policy.last_access_expiry {
405 transaction
406 .delete_media_by_last_access_earlier_than(ignore_policy, current_time - expiry)
407 .await?;
408 }
409
410 if let Some(max_cache_size) = policy.max_cache_size {
411 let cache_size = transaction
412 .get_cache_size(ignore_policy)
413 .await?
414 .ok_or(Self::Error::CacheSizeTooBig)?;
415 if cache_size > (max_cache_size as usize) {
416 let (_, upper_key) = transaction
417 .fold_media_metadata_keys_by_retention_while(
418 CursorDirection::Prev,
419 ignore_policy,
420 0usize,
421 |total, key| match total.checked_add(key.content_size()) {
422 None => None,
423 Some(total) if total > max_cache_size as usize => None,
424 Some(total) => Some(total),
425 },
426 )
427 .await?;
428 if let Some(upper_key) = upper_key {
429 transaction
430 .delete_media_by_retention_metadata_to(
431 upper_key.ignore_policy(),
432 upper_key.last_access(),
433 upper_key.content_size(),
434 )
435 .await?;
436 }
437 }
438 }
439
440 transaction.put_media_cleanup_time(current_time).await?;
441 transaction.commit().await.map_err(Into::into)
442 }
443
444 #[instrument(skip_all)]
445 async fn last_media_cleanup_time_inner(
446 &self,
447 ) -> Result<Option<SystemTime>, IndexeddbMediaStoreError> {
448 let _timer = timer!("method");
449 let time = self
450 .transaction(&[MediaCleanupTime::OBJECT_STORE], TransactionMode::Readonly)?
451 .get_media_cleanup_time()
452 .await?;
453 Ok(time.map(Into::into))
454 }
455}
456
457#[cfg(all(test, target_family = "wasm"))]
458mod tests {
459 use matrix_sdk_base::{
460 media::store::MediaStoreError, media_store_inner_integration_tests,
461 media_store_integration_tests, media_store_integration_tests_time,
462 };
463 use uuid::Uuid;
464
465 use crate::media_store::IndexeddbMediaStore;
466
467 mod unencrypted {
468 use super::*;
469
470 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
471
472 async fn get_media_store() -> Result<IndexeddbMediaStore, MediaStoreError> {
473 let name = format!("test-media-store-{}", Uuid::new_v4().as_hyphenated());
474 Ok(IndexeddbMediaStore::builder().database_name(name).build().await?)
475 }
476
477 #[cfg(target_family = "wasm")]
478 media_store_integration_tests!();
479
480 #[cfg(target_family = "wasm")]
481 media_store_integration_tests_time!();
482
483 #[cfg(target_family = "wasm")]
484 media_store_inner_integration_tests!(with_media_size_tests);
485 }
486
487 mod encrypted {
488 use std::sync::Arc;
489
490 use matrix_sdk_store_encryption::StoreCipher;
491
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()
499 .database_name(name)
500 .store_cipher(Arc::new(StoreCipher::new().expect("store cipher")))
501 .build()
502 .await?)
503 }
504
505 #[cfg(target_family = "wasm")]
506 media_store_integration_tests!();
507
508 #[cfg(target_family = "wasm")]
509 media_store_integration_tests_time!();
510
511 #[cfg(target_family = "wasm")]
512 media_store_inner_integration_tests!();
513 }
514}