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