mirror of
https://github.com/matrix-org/matrix-rust-sdk.git
synced 2026-06-22 07:08:32 -04:00
indexeddb: move v8_to_v10 migration into its own file
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
This commit is contained in:
@@ -22,6 +22,8 @@ use crate::{
|
||||
IndexeddbCryptoStoreError,
|
||||
};
|
||||
|
||||
mod v8_to_v10;
|
||||
|
||||
mod old_keys {
|
||||
/// Old format of the `inbound_group_sessions` store which lacked indexes or
|
||||
/// a sensible structure
|
||||
@@ -416,188 +418,6 @@ async fn prepare_data_for_v8(name: &str, serializer: &IndexeddbSerializer) -> Re
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Migration code that moves from inbound_group_sessions2 to
|
||||
/// inbound_group_sessions3, shrinking the values stored in each record.
|
||||
///
|
||||
/// The migration 8->9 creates the new store inbound_group_sessions3.
|
||||
/// Then we move the data into the new store.
|
||||
/// The migration 9->10 deletes the old store inbound_group_sessions2.
|
||||
mod v8_to_v10 {
|
||||
use indexed_db_futures::{
|
||||
idb_object_store::IdbObjectStore,
|
||||
request::{IdbOpenDbRequestLike, OpenDbRequest},
|
||||
IdbDatabase, IdbIndex, IdbKeyPath, IdbQuerySource, IdbVersionChangeEvent,
|
||||
};
|
||||
use matrix_sdk_crypto::olm::InboundGroupSession;
|
||||
use tracing::{debug, info};
|
||||
use wasm_bindgen::JsValue;
|
||||
use web_sys::{DomException, IdbIndexParameters, IdbTransactionMode};
|
||||
|
||||
use super::Result;
|
||||
use crate::{
|
||||
crypto_store::{
|
||||
indexeddb_serializer::IndexeddbSerializer, keys, migrations::old_keys,
|
||||
InboundGroupSessionIndexedDbObject,
|
||||
},
|
||||
IndexeddbCryptoStoreError,
|
||||
};
|
||||
|
||||
/// The objects we store in the inbound_group_sessions2 indexeddb object
|
||||
/// store (in schemas v7 and v8)
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct InboundGroupSessionIndexedDbObject2 {
|
||||
/// (Possibly encrypted) serialisation of a
|
||||
/// [`matrix_sdk_crypto::olm::group_sessions::PickledInboundGroupSession`]
|
||||
/// structure.
|
||||
pub pickled_session: Vec<u8>,
|
||||
|
||||
/// Whether the session data has yet to be backed up.
|
||||
///
|
||||
/// Since we only need to be able to find entries where this is `true`,
|
||||
/// we skip serialization in cases where it is `false`. That has
|
||||
/// the effect of omitting it from the indexeddb index.
|
||||
///
|
||||
/// We also use a custom serializer because bools can't be used as keys
|
||||
/// in indexeddb.
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "std::ops::Not::not",
|
||||
with = "crate::serialize_bool_for_indexeddb"
|
||||
)]
|
||||
pub needs_backup: bool,
|
||||
}
|
||||
|
||||
fn add_nonunique_index<'a>(
|
||||
object_store: &'a IdbObjectStore<'a>,
|
||||
name: &str,
|
||||
key_path: &str,
|
||||
) -> Result<IdbIndex<'a>, DomException> {
|
||||
let mut params = IdbIndexParameters::new();
|
||||
params.unique(false);
|
||||
object_store.create_index_with_params(name, &IdbKeyPath::str(key_path), ¶ms)
|
||||
}
|
||||
|
||||
async fn do_schema_upgrade<F>(name: &str, version: u32, f: F) -> Result<(), DomException>
|
||||
where
|
||||
F: Fn(&IdbDatabase) -> Result<(), JsValue> + 'static,
|
||||
{
|
||||
info!("IndexeddbCryptoStore upgrade schema -> v{version} starting");
|
||||
let mut db_req: OpenDbRequest = IdbDatabase::open_u32(name, version)?;
|
||||
|
||||
db_req.set_on_upgrade_needed(Some(move |evt: &IdbVersionChangeEvent| f(evt.db())));
|
||||
|
||||
let db = db_req.await?;
|
||||
db.close();
|
||||
info!("IndexeddbCryptoStore upgrade schema -> v{version} complete");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn upgrade_scheme_to_v9_create_inbound_group_sessions3(
|
||||
name: &str,
|
||||
) -> Result<(), DomException> {
|
||||
do_schema_upgrade(name, 9, |db| {
|
||||
let object_store = db.create_object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
|
||||
|
||||
add_nonunique_index(
|
||||
&object_store,
|
||||
keys::INBOUND_GROUP_SESSIONS_BACKUP_INDEX,
|
||||
"needs_backup",
|
||||
)?;
|
||||
|
||||
// See https://github.com/element-hq/element-web/issues/26892#issuecomment-1906336076
|
||||
// for the plan concerning this property and index. At time of writing, it is
|
||||
// unused, and needs_backup is still used.
|
||||
add_nonunique_index(
|
||||
&object_store,
|
||||
keys::INBOUND_GROUP_SESSIONS_BACKED_UP_TO_INDEX,
|
||||
"backed_up_to",
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn migrate_data_before_v10_populate_inbound_group_sessions3(
|
||||
name: &str,
|
||||
serializer: &IndexeddbSerializer,
|
||||
) -> Result<()> {
|
||||
info!("IndexeddbCryptoStore migrate data before v10 starting");
|
||||
|
||||
let db = IdbDatabase::open(name)?.await?;
|
||||
let txn = db.transaction_on_multi_with_mode(
|
||||
&[old_keys::INBOUND_GROUP_SESSIONS_V2, keys::INBOUND_GROUP_SESSIONS_V3],
|
||||
IdbTransactionMode::Readwrite,
|
||||
)?;
|
||||
|
||||
let inbound_group_sessions2 = txn.object_store(old_keys::INBOUND_GROUP_SESSIONS_V2)?;
|
||||
let inbound_group_sessions3 = txn.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
|
||||
|
||||
let row_count = inbound_group_sessions2.count()?.await?;
|
||||
info!(row_count, "Shrinking inbound_group_session records");
|
||||
|
||||
// Iterate through all rows
|
||||
if let Some(cursor) = inbound_group_sessions2.open_cursor()?.await? {
|
||||
let mut idx = 0;
|
||||
loop {
|
||||
idx += 1;
|
||||
|
||||
if idx % 100 == 0 {
|
||||
debug!("Migrating session {idx} of {row_count}");
|
||||
}
|
||||
|
||||
// Deserialize the session from the old store
|
||||
let old_value: InboundGroupSessionIndexedDbObject2 =
|
||||
serde_wasm_bindgen::from_value(cursor.value())?;
|
||||
|
||||
let session = InboundGroupSession::from_pickle(
|
||||
serializer.deserialize_value_from_bytes(&old_value.pickled_session)?,
|
||||
)
|
||||
.map_err(|e| IndexeddbCryptoStoreError::CryptoStoreError(e.into()))?;
|
||||
|
||||
// Calculate its key in the new table
|
||||
let new_key = serializer.encode_key(
|
||||
keys::INBOUND_GROUP_SESSIONS_V3,
|
||||
(&session.room_id, session.session_id()),
|
||||
);
|
||||
|
||||
// Serialize the session in the new format
|
||||
// This is much the same as [`IndexeddbStore::serialize_inbound_group_session`].
|
||||
let new_value = InboundGroupSessionIndexedDbObject::new(
|
||||
serializer.maybe_encrypt_value(session.pickle().await)?,
|
||||
!session.backed_up(),
|
||||
);
|
||||
|
||||
// Write it to the new store
|
||||
inbound_group_sessions3
|
||||
.add_key_val(&new_key, &serde_wasm_bindgen::to_value(&new_value)?)?;
|
||||
|
||||
// Continue to the next record, or stop if we're done
|
||||
if !cursor.continue_cursor()?.await? {
|
||||
debug!("Migrated {idx} sessions.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
txn.await.into_result()?;
|
||||
db.close();
|
||||
info!("IndexeddbCryptoStore upgrade data before v10 finished");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn upgrade_scheme_to_v10_delete_inbound_group_sessions2(
|
||||
name: &str,
|
||||
) -> Result<(), DomException> {
|
||||
do_schema_upgrade(name, 10, |db| {
|
||||
db.delete_object_store(old_keys::INBOUND_GROUP_SESSIONS_V2)?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, target_arch = "wasm32"))]
|
||||
mod tests {
|
||||
use std::{future::Future, sync::Arc};
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
// Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Migration code that moves from inbound_group_sessions2 to
|
||||
//! inbound_group_sessions3, shrinking the values stored in each record.
|
||||
//!
|
||||
//! The migration 8->9 creates the new store inbound_group_sessions3.
|
||||
//! Then we move the data into the new store.
|
||||
//! The migration 9->10 deletes the old store inbound_group_sessions2.
|
||||
|
||||
use indexed_db_futures::{
|
||||
idb_object_store::IdbObjectStore,
|
||||
request::{IdbOpenDbRequestLike, OpenDbRequest},
|
||||
IdbDatabase, IdbIndex, IdbKeyPath, IdbQuerySource, IdbVersionChangeEvent,
|
||||
};
|
||||
use matrix_sdk_crypto::olm::InboundGroupSession;
|
||||
use tracing::{debug, info};
|
||||
use wasm_bindgen::JsValue;
|
||||
use web_sys::{DomException, IdbIndexParameters, IdbTransactionMode};
|
||||
|
||||
use super::Result;
|
||||
use crate::{
|
||||
crypto_store::{
|
||||
indexeddb_serializer::IndexeddbSerializer, keys, migrations::old_keys,
|
||||
InboundGroupSessionIndexedDbObject,
|
||||
},
|
||||
IndexeddbCryptoStoreError,
|
||||
};
|
||||
|
||||
/// The objects we store in the inbound_group_sessions2 indexeddb object
|
||||
/// store (in schemas v7 and v8)
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct InboundGroupSessionIndexedDbObject2 {
|
||||
/// (Possibly encrypted) serialisation of a
|
||||
/// [`matrix_sdk_crypto::olm::group_sessions::PickledInboundGroupSession`]
|
||||
/// structure.
|
||||
pub pickled_session: Vec<u8>,
|
||||
|
||||
/// Whether the session data has yet to be backed up.
|
||||
///
|
||||
/// Since we only need to be able to find entries where this is `true`,
|
||||
/// we skip serialization in cases where it is `false`. That has
|
||||
/// the effect of omitting it from the indexeddb index.
|
||||
///
|
||||
/// We also use a custom serializer because bools can't be used as keys
|
||||
/// in indexeddb.
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "std::ops::Not::not",
|
||||
with = "crate::serialize_bool_for_indexeddb"
|
||||
)]
|
||||
pub needs_backup: bool,
|
||||
}
|
||||
|
||||
fn add_nonunique_index<'a>(
|
||||
object_store: &'a IdbObjectStore<'a>,
|
||||
name: &str,
|
||||
key_path: &str,
|
||||
) -> Result<IdbIndex<'a>, DomException> {
|
||||
let mut params = IdbIndexParameters::new();
|
||||
params.unique(false);
|
||||
object_store.create_index_with_params(name, &IdbKeyPath::str(key_path), ¶ms)
|
||||
}
|
||||
|
||||
async fn do_schema_upgrade<F>(name: &str, version: u32, f: F) -> Result<(), DomException>
|
||||
where
|
||||
F: Fn(&IdbDatabase) -> Result<(), JsValue> + 'static,
|
||||
{
|
||||
info!("IndexeddbCryptoStore upgrade schema -> v{version} starting");
|
||||
let mut db_req: OpenDbRequest = IdbDatabase::open_u32(name, version)?;
|
||||
|
||||
db_req.set_on_upgrade_needed(Some(move |evt: &IdbVersionChangeEvent| f(evt.db())));
|
||||
|
||||
let db = db_req.await?;
|
||||
db.close();
|
||||
info!("IndexeddbCryptoStore upgrade schema -> v{version} complete");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn upgrade_scheme_to_v9_create_inbound_group_sessions3(
|
||||
name: &str,
|
||||
) -> Result<(), DomException> {
|
||||
do_schema_upgrade(name, 9, |db| {
|
||||
let object_store = db.create_object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
|
||||
|
||||
add_nonunique_index(
|
||||
&object_store,
|
||||
keys::INBOUND_GROUP_SESSIONS_BACKUP_INDEX,
|
||||
"needs_backup",
|
||||
)?;
|
||||
|
||||
// See https://github.com/element-hq/element-web/issues/26892#issuecomment-1906336076
|
||||
// for the plan concerning this property and index. At time of writing, it is
|
||||
// unused, and needs_backup is still used.
|
||||
add_nonunique_index(
|
||||
&object_store,
|
||||
keys::INBOUND_GROUP_SESSIONS_BACKED_UP_TO_INDEX,
|
||||
"backed_up_to",
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn migrate_data_before_v10_populate_inbound_group_sessions3(
|
||||
name: &str,
|
||||
serializer: &IndexeddbSerializer,
|
||||
) -> Result<()> {
|
||||
info!("IndexeddbCryptoStore migrate data before v10 starting");
|
||||
|
||||
let db = IdbDatabase::open(name)?.await?;
|
||||
let txn = db.transaction_on_multi_with_mode(
|
||||
&[old_keys::INBOUND_GROUP_SESSIONS_V2, keys::INBOUND_GROUP_SESSIONS_V3],
|
||||
IdbTransactionMode::Readwrite,
|
||||
)?;
|
||||
|
||||
let inbound_group_sessions2 = txn.object_store(old_keys::INBOUND_GROUP_SESSIONS_V2)?;
|
||||
let inbound_group_sessions3 = txn.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
|
||||
|
||||
let row_count = inbound_group_sessions2.count()?.await?;
|
||||
info!(row_count, "Shrinking inbound_group_session records");
|
||||
|
||||
// Iterate through all rows
|
||||
if let Some(cursor) = inbound_group_sessions2.open_cursor()?.await? {
|
||||
let mut idx = 0;
|
||||
loop {
|
||||
idx += 1;
|
||||
|
||||
if idx % 100 == 0 {
|
||||
debug!("Migrating session {idx} of {row_count}");
|
||||
}
|
||||
|
||||
// Deserialize the session from the old store
|
||||
let old_value: InboundGroupSessionIndexedDbObject2 =
|
||||
serde_wasm_bindgen::from_value(cursor.value())?;
|
||||
|
||||
let session = InboundGroupSession::from_pickle(
|
||||
serializer.deserialize_value_from_bytes(&old_value.pickled_session)?,
|
||||
)
|
||||
.map_err(|e| IndexeddbCryptoStoreError::CryptoStoreError(e.into()))?;
|
||||
|
||||
// Calculate its key in the new table
|
||||
let new_key = serializer.encode_key(
|
||||
keys::INBOUND_GROUP_SESSIONS_V3,
|
||||
(&session.room_id, session.session_id()),
|
||||
);
|
||||
|
||||
// Serialize the session in the new format
|
||||
// This is much the same as [`IndexeddbStore::serialize_inbound_group_session`].
|
||||
let new_value = InboundGroupSessionIndexedDbObject::new(
|
||||
serializer.maybe_encrypt_value(session.pickle().await)?,
|
||||
!session.backed_up(),
|
||||
);
|
||||
|
||||
// Write it to the new store
|
||||
inbound_group_sessions3
|
||||
.add_key_val(&new_key, &serde_wasm_bindgen::to_value(&new_value)?)?;
|
||||
|
||||
// Continue to the next record, or stop if we're done
|
||||
if !cursor.continue_cursor()?.await? {
|
||||
debug!("Migrated {idx} sessions.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
txn.await.into_result()?;
|
||||
db.close();
|
||||
info!("IndexeddbCryptoStore upgrade data before v10 finished");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn upgrade_scheme_to_v10_delete_inbound_group_sessions2(
|
||||
name: &str,
|
||||
) -> Result<(), DomException> {
|
||||
do_schema_upgrade(name, 10, |db| {
|
||||
db.delete_object_store(old_keys::INBOUND_GROUP_SESSIONS_V2)?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
Reference in New Issue
Block a user