Skip to main content

matrix_sdk_indexeddb/media_store/
migrations.rs

1// Copyright 2025 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License
14
15use indexed_db_futures::{
16    database::Database,
17    error::{DomException, Error, OpenDbError},
18    transaction::Transaction,
19};
20use thiserror::Error;
21
22/// The current version and keys used in the database.
23pub mod current {
24    use super::{Version, v2};
25
26    pub const VERSION: Version = Version::V2;
27    pub use v2::keys;
28}
29
30/// Opens a connection to the IndexedDB database and takes care of upgrading it
31/// if necessary.
32#[allow(unused)]
33pub async fn open_and_upgrade_db(name: &str) -> Result<Database, OpenDbError> {
34    Database::open(name)
35        .with_version(current::VERSION as u32)
36        .with_on_upgrade_needed(|event, transaction| {
37            let mut version = Version::try_from(event.old_version() as u32)?;
38            while version < current::VERSION {
39                version = match version.upgrade(transaction)? {
40                    Some(next) => next,
41                    None => current::VERSION, /* No more upgrades to apply, jump forward! */
42                };
43            }
44            Ok(())
45        })
46        .await
47}
48
49/// Represents the version of the IndexedDB database.
50#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
51#[repr(u32)]
52pub enum Version {
53    /// Version 0 of the database, for details see [`v0`].
54    V0 = 0,
55    /// Version 1 of the database, for details see [`v1`].
56    V1 = 1,
57    /// Version 2 of the database, for details see [`v2`].
58    V2 = 2,
59}
60
61impl Version {
62    /// Upgrade the database to the next version, if one exists.
63    pub fn upgrade(self, transaction: &Transaction<'_>) -> Result<Option<Self>, Error> {
64        match self {
65            Self::V0 => v0::upgrade(transaction).map(Some),
66            Self::V1 => v1::upgrade(transaction).map(Some),
67            Self::V2 => Ok(None),
68        }
69    }
70}
71
72#[derive(Debug, Error)]
73#[error("unknown version: {0}")]
74pub struct UnknownVersionError(u32);
75
76impl TryFrom<u32> for Version {
77    type Error = UnknownVersionError;
78
79    fn try_from(value: u32) -> Result<Self, Self::Error> {
80        match value {
81            0 => Ok(Version::V0),
82            1 => Ok(Version::V1),
83            2 => Ok(Version::V2),
84            v => Err(UnknownVersionError(v)),
85        }
86    }
87}
88
89impl From<UnknownVersionError> for Error {
90    fn from(value: UnknownVersionError) -> Self {
91        let message = format!("unknown version: {}", value.0);
92        let name = "UnknownVersionError";
93        match web_sys::DomException::new_with_message_and_name(&message, name) {
94            Ok(inner) => Self::DomException(DomException::DataError(inner)),
95            Err(err) => err.into(),
96        }
97    }
98}
99
100pub mod v0 {
101    use super::*;
102
103    /// Upgrade database from `v0` to `v1`
104    pub fn upgrade(transaction: &Transaction<'_>) -> Result<Version, Error> {
105        v1::create_object_stores(transaction.db())?;
106        Ok(Version::V1)
107    }
108}
109
110pub mod v1 {
111    use indexed_db_futures::Build;
112
113    use super::*;
114
115    pub mod keys {
116        pub const CORE: &str = "core";
117        pub const CORE_KEY_PATH: &str = "id";
118        pub const LEASES: &str = "leases";
119        pub const LEASES_KEY_PATH: &str = "id";
120        pub const MEDIA_RETENTION_POLICY_KEY: &str = "media_retention_policy";
121        pub const MEDIA_CLEANUP_TIME_KEY: &str = "media_cleanup_time";
122        pub const MEDIA_METADATA: &str = "media_metadata";
123        pub const MEDIA_METADATA_KEY_PATH: &str = "id";
124        pub const MEDIA_METADATA_URI: &str = "media_metadata_uri";
125        pub const MEDIA_METADATA_URI_KEY_PATH: &str = "uri";
126        pub const MEDIA_METADATA_CONTENT_SIZE: &str = "media_metadata_content_size";
127        pub const MEDIA_METADATA_CONTENT_SIZE_KEY_PATH: &str = "content_size";
128        pub const MEDIA_METADATA_LAST_ACCESS: &str = "media_metadata_last_access";
129        pub const MEDIA_METADATA_LAST_ACCESS_KEY_PATH: &str = "last_access";
130        pub const MEDIA_METADATA_RETENTION: &str = "media_metadata_retention";
131        pub const MEDIA_METADATA_RETENTION_KEY_PATH: &str = "retention";
132        pub const MEDIA_CONTENT: &str = "media_content";
133        pub const MEDIA_CONTENT_KEY_PATH: &str = "id";
134    }
135
136    /// Create all object stores and indices for v1 database
137    pub fn create_object_stores(db: &Database) -> Result<(), Error> {
138        create_core_object_store(db)?;
139        create_lease_object_store(db)?;
140        create_media_metadata_object_store(db)?;
141        create_media_content_object_store(db)?;
142        Ok(())
143    }
144
145    /// Create an object store for tracking miscellaneous information
146    ///
147    /// * Primary Key - `id`
148    fn create_core_object_store(db: &Database) -> Result<(), Error> {
149        let _ =
150            db.create_object_store(keys::CORE).with_key_path(keys::CORE_KEY_PATH.into()).build()?;
151        Ok(())
152    }
153
154    /// Create an object store tracking leases on time-based locks
155    fn create_lease_object_store(db: &Database) -> Result<(), Error> {
156        let _ = db
157            .create_object_store(keys::LEASES)
158            .with_key_path(keys::LEASES_KEY_PATH.into())
159            .build()?;
160        Ok(())
161    }
162
163    /// Create an object store for tracking information about media metadata.
164    ///
165    /// * Primary Key - `id` - unique key derived from
166    ///   [`MediaRequestParameters`] of the associated media
167    /// * Index - `uri` - tracks the [`MxcUri`][1] of the associated media
168    /// * Index - `content_size` - tracks the size of the media content and
169    ///   whether to ignore the [`MediaRetentionPolicy`][2]
170    /// * Index - `last_access` - tracks the last time the associated media was
171    ///   accessed
172    /// * Index - `retention` - tracks all retention metadata - i.e., joins
173    ///   `content_size` and `last_access`
174    ///
175    /// [1]: ruma::MxcUri
176    /// [2]: matrix_sdk_base::media::store::MediaRetentionPolicy
177    fn create_media_metadata_object_store(db: &Database) -> Result<(), Error> {
178        let media = db
179            .create_object_store(keys::MEDIA_METADATA)
180            .with_key_path(keys::MEDIA_METADATA_KEY_PATH.into())
181            .build()?;
182        let _ = media
183            .create_index(keys::MEDIA_METADATA_URI, keys::MEDIA_METADATA_URI_KEY_PATH.into())
184            .build()?;
185        let _ = media
186            .create_index(
187                keys::MEDIA_METADATA_CONTENT_SIZE,
188                keys::MEDIA_METADATA_CONTENT_SIZE_KEY_PATH.into(),
189            )
190            .build()?;
191        let _ = media
192            .create_index(
193                keys::MEDIA_METADATA_LAST_ACCESS,
194                keys::MEDIA_METADATA_LAST_ACCESS_KEY_PATH.into(),
195            )
196            .build()?;
197        let _ = media
198            .create_index(
199                keys::MEDIA_METADATA_RETENTION,
200                keys::MEDIA_METADATA_RETENTION_KEY_PATH.into(),
201            )
202            .build()?;
203        Ok(())
204    }
205
206    /// Create an object store for tracking information about media.
207    ///
208    /// * Primary Key - `id` - UUID tracking the ID of the media content.
209    fn create_media_content_object_store(db: &Database) -> Result<(), Error> {
210        let _ = db
211            .create_object_store(keys::MEDIA_CONTENT)
212            .with_key_path(keys::MEDIA_CONTENT_KEY_PATH.into())
213            .build()?;
214        Ok(())
215    }
216
217    /// Upgrade database from `v1` to `v2`
218    pub fn upgrade(transaction: &Transaction<'_>) -> Result<Version, Error> {
219        v2::empty_leases(transaction)?;
220        Ok(Version::V2)
221    }
222}
223
224mod v2 {
225    // Re-use all the same keys from `v1`.
226    pub use super::v1::keys;
227    use super::*;
228
229    /// The format of [`Lease`][super::super::types::Lease] is changing. Let's
230    /// erase previous values.
231    pub fn empty_leases(transaction: &Transaction<'_>) -> Result<(), Error> {
232        let object_store = transaction.object_store(keys::LEASES)?;
233
234        // Remove all previous leases.
235        object_store.clear()?;
236
237        Ok(())
238    }
239}