refactor: Add delete database method

This adds a delete database
method used for migration to
correctly delete the whole
legacy database instead just
empty it.
This commit is contained in:
Krille 2023-12-12 08:30:00 +01:00
parent b3ec966238
commit eb869462aa
No known key found for this signature in database
10 changed files with 165 additions and 112 deletions

View File

@ -3006,123 +3006,123 @@ class Client extends MatrixApi {
final migrateClient = await legacyDatabase?.getClient(clientName); final migrateClient = await legacyDatabase?.getClient(clientName);
final database = this.database; final database = this.database;
if (migrateClient != null && legacyDatabase != null && database != null) { if (migrateClient == null || legacyDatabase == null || database == null) {
Logs().i('Found data in the legacy database!'); await legacyDatabase?.close();
onMigration?.call(); _initLock = false;
_id = migrateClient['client_id']; return;
await database.insertClient( }
clientName, Logs().i('Found data in the legacy database!');
migrateClient['homeserver_url'], onMigration?.call();
migrateClient['token'], _id = migrateClient['client_id'];
migrateClient['user_id'], await database.insertClient(
migrateClient['device_id'], clientName,
migrateClient['device_name'], migrateClient['homeserver_url'],
null, migrateClient['token'],
migrateClient['olm_account'], migrateClient['user_id'],
); migrateClient['device_id'],
Logs().d('Migrate SSSSCache...'); migrateClient['device_name'],
for (final type in cacheTypes) { null,
final ssssCache = await legacyDatabase.getSSSSCache(type); migrateClient['olm_account'],
if (ssssCache != null) { );
Logs().d('Migrate $type...'); Logs().d('Migrate SSSSCache...');
await database.storeSSSSCache( for (final type in cacheTypes) {
type, final ssssCache = await legacyDatabase.getSSSSCache(type);
ssssCache.keyId ?? '', if (ssssCache != null) {
ssssCache.ciphertext ?? '', Logs().d('Migrate $type...');
ssssCache.content ?? '', await database.storeSSSSCache(
type,
ssssCache.keyId ?? '',
ssssCache.ciphertext ?? '',
ssssCache.content ?? '',
);
}
}
Logs().d('Migrate OLM sessions...');
try {
final olmSessions = await legacyDatabase.getAllOlmSessions();
for (final identityKey in olmSessions.keys) {
final sessions = olmSessions[identityKey]!;
for (final sessionId in sessions.keys) {
final session = sessions[sessionId]!;
await database.storeOlmSession(
identityKey,
session['session_id'] as String,
session['pickle'] as String,
session['last_received'] as int,
); );
} }
} }
Logs().d('Migrate OLM sessions...'); } catch (e, s) {
try { Logs().e('Unable to migrate OLM sessions!', e, s);
final olmSessions = await legacyDatabase.getAllOlmSessions(); }
for (final identityKey in olmSessions.keys) { Logs().d('Migrate Device Keys...');
final sessions = olmSessions[identityKey]!; final userDeviceKeys = await legacyDatabase.getUserDeviceKeys(this);
for (final sessionId in sessions.keys) { for (final userId in userDeviceKeys.keys) {
final session = sessions[sessionId]!; Logs().d('Migrate Device Keys of user $userId...');
await database.storeOlmSession( final deviceKeysList = userDeviceKeys[userId];
identityKey, for (final crossSigningKey
session['session_id'] as String, in deviceKeysList?.crossSigningKeys.values ?? <CrossSigningKey>[]) {
session['pickle'] as String, final pubKey = crossSigningKey.publicKey;
session['last_received'] as int, if (pubKey != null) {
); Logs().d(
} 'Migrate cross signing key with usage ${crossSigningKey.usage} and verified ${crossSigningKey.directVerified}...');
await database.storeUserCrossSigningKey(
userId,
pubKey,
jsonEncode(crossSigningKey.toJson()),
crossSigningKey.directVerified,
crossSigningKey.blocked,
);
} }
} catch (e, s) {
Logs().e('Unable to migrate OLM sessions!', e, s);
} }
Logs().d('Migrate Device Keys...');
final userDeviceKeys = await legacyDatabase.getUserDeviceKeys(this); if (deviceKeysList != null) {
for (final userId in userDeviceKeys.keys) { for (final deviceKeys in deviceKeysList.deviceKeys.values) {
Logs().d('Migrate Device Keys of user $userId...'); final deviceId = deviceKeys.deviceId;
final deviceKeysList = userDeviceKeys[userId]; if (deviceId != null) {
for (final crossSigningKey Logs().d('Migrate device keys for ${deviceKeys.deviceId}...');
in deviceKeysList?.crossSigningKeys.values ?? <CrossSigningKey>[]) { await database.storeUserDeviceKey(
final pubKey = crossSigningKey.publicKey;
if (pubKey != null) {
Logs().d(
'Migrate cross signing key with usage ${crossSigningKey.usage} and verified ${crossSigningKey.directVerified}...');
await database.storeUserCrossSigningKey(
userId, userId,
pubKey, deviceId,
jsonEncode(crossSigningKey.toJson()), jsonEncode(deviceKeys.toJson()),
crossSigningKey.directVerified, deviceKeys.directVerified,
crossSigningKey.blocked, deviceKeys.blocked,
deviceKeys.lastActive.millisecondsSinceEpoch,
); );
} }
} }
Logs().d('Migrate user device keys info...');
if (deviceKeysList != null) { await database.storeUserDeviceKeysInfo(userId, deviceKeysList.outdated);
for (final deviceKeys in deviceKeysList.deviceKeys.values) {
final deviceId = deviceKeys.deviceId;
if (deviceId != null) {
Logs().d('Migrate device keys for ${deviceKeys.deviceId}...');
await database.storeUserDeviceKey(
userId,
deviceId,
jsonEncode(deviceKeys.toJson()),
deviceKeys.directVerified,
deviceKeys.blocked,
deviceKeys.lastActive.millisecondsSinceEpoch,
);
}
}
Logs().d('Migrate user device keys info...');
await database.storeUserDeviceKeysInfo(
userId, deviceKeysList.outdated);
}
} }
Logs().d('Migrate inbound group sessions...');
try {
final sessions = await legacyDatabase.getAllInboundGroupSessions();
for (var i = 0; i < sessions.length; i++) {
Logs().d('$i / ${sessions.length}');
final session = sessions[i];
await database.storeInboundGroupSession(
session.roomId,
session.sessionId,
session.pickle,
session.content,
session.indexes,
session.allowedAtIndex,
session.senderKey,
session.senderClaimedKeys,
);
}
} catch (e, s) {
Logs().e('Unable to migrate inbound group sessions!', e, s);
}
await legacyDatabase.clear();
} }
await legacyDatabase?.close(); Logs().d('Migrate inbound group sessions...');
try {
final sessions = await legacyDatabase.getAllInboundGroupSessions();
for (var i = 0; i < sessions.length; i++) {
Logs().d('$i / ${sessions.length}');
final session = sessions[i];
await database.storeInboundGroupSession(
session.roomId,
session.sessionId,
session.pickle,
session.content,
session.indexes,
session.allowedAtIndex,
session.senderKey,
session.senderClaimedKeys,
);
}
} catch (e, s) {
Logs().e('Unable to migrate inbound group sessions!', e, s);
}
await legacyDatabase.delete();
_initLock = false; _initLock = false;
if (migrateClient != null) { return init(
return init( waitForFirstSync: false,
waitForFirstSync: false, waitUntilLoadCompletedLoaded: false,
waitUntilLoadCompletedLoaded: false, );
);
}
} }
} }

View File

@ -316,4 +316,8 @@ abstract class DatabaseApi {
Future<void> storePresence(String userId, CachedPresence presence); Future<void> storePresence(String userId, CachedPresence presence);
Future<CachedPresence?> getPresence(String userId); Future<CachedPresence?> getPresence(String userId);
/// Deletes the whole database. The database needs to be created again after
/// this. Used for migration only.
Future<void> delete();
} }

View File

@ -1594,6 +1594,9 @@ class HiveCollectionsDatabase extends DatabaseApi {
return false; return false;
} }
} }
@override
Future<void> delete() => _collection.deleteFromDisk();
} }
class TupleKey { class TupleKey {

View File

@ -1474,6 +1474,9 @@ class FamedlySdkHiveDatabase extends DatabaseApi {
// see no need to implement this in a deprecated part // see no need to implement this in a deprecated part
throw UnimplementedError(); throw UnimplementedError();
} }
@override
Future<void> delete() => Hive.deleteFromDisk();
} }
dynamic _castValue(dynamic value) { dynamic _castValue(dynamic value) {

View File

@ -7,13 +7,16 @@ import 'dart:indexed_db';
class BoxCollection { class BoxCollection {
final Database _db; final Database _db;
final Set<String> boxNames; final Set<String> boxNames;
final String _name;
final IdbFactory _idbFactory;
BoxCollection(this._db, this.boxNames); BoxCollection(this._db, this.boxNames, this._name, this._idbFactory);
static Future<BoxCollection> open( static Future<BoxCollection> open(
String name, String name,
Set<String> boxNames, { Set<String> boxNames, {
Object? sqfliteDatabase, Object? sqfliteDatabase,
Object? sqfliteFactory,
IdbFactory? idbFactory, IdbFactory? idbFactory,
}) async { }) async {
idbFactory ??= window.indexedDB!; idbFactory ??= window.indexedDB!;
@ -24,7 +27,7 @@ class BoxCollection {
db.createObjectStore(name, autoIncrement: true); db.createObjectStore(name, autoIncrement: true);
} }
}); });
return BoxCollection(db, boxNames); return BoxCollection(db, boxNames, name, idbFactory);
} }
Box<V> openBox<V>(String name) { Box<V> openBox<V>(String name) {
@ -61,15 +64,19 @@ class BoxCollection {
} }
Future<void> clear() async { Future<void> clear() async {
final txn = _db.transaction(boxNames, 'readwrite');
for (final name in boxNames) { for (final name in boxNames) {
_db.deleteObjectStore(name); unawaited(txn.objectStore(name).clear());
} }
await txn.completed;
} }
Future<void> close() async { Future<void> close() async {
assert(_txnCache == null, 'Database closed while in transaction!'); assert(_txnCache == null, 'Database closed while in transaction!');
return _db.close(); return _db.close();
} }
Future<void> delete() => _idbFactory.deleteDatabase(_name);
} }
class Box<V> { class Box<V> {

View File

@ -144,10 +144,15 @@ class MatrixSdkDatabase extends DatabaseApi {
/// typed. /// typed.
final dynamic idbFactory; final dynamic idbFactory;
/// Custom SQFlite Database Factory used for high level operations on IO
/// like delete. Set it if you want to use sqlite FFI.
final DatabaseFactory? sqfliteFactory;
MatrixSdkDatabase( MatrixSdkDatabase(
this.name, { this.name, {
this.database, this.database,
this.idbFactory, this.idbFactory,
this.sqfliteFactory,
this.maxFileSize = 0, this.maxFileSize = 0,
this.fileStoragePath, this.fileStoragePath,
this.deleteFilesAfterDuration, this.deleteFilesAfterDuration,
@ -179,6 +184,7 @@ class MatrixSdkDatabase extends DatabaseApi {
_seenDeviceKeysBoxName, _seenDeviceKeysBoxName,
}, },
sqfliteDatabase: database, sqfliteDatabase: database,
sqfliteFactory: sqfliteFactory,
idbFactory: idbFactory, idbFactory: idbFactory,
); );
_clientBox = _collection.openBox<String>( _clientBox = _collection.openBox<String>(
@ -1596,4 +1602,7 @@ class MatrixSdkDatabase extends DatabaseApi {
return CachedPresence.fromJson(copyMap(rawPresence)); return CachedPresence.fromJson(copyMap(rawPresence));
} }
@override
Future<void> delete() => _collection.delete();
} }

View File

@ -1,20 +1,22 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:sqflite_common/sqlite_api.dart'; import 'package:sqflite_common/sqflite.dart';
/// Key-Value store abstraction over Sqflite so that the sdk database can use /// Key-Value store abstraction over Sqflite so that the sdk database can use
/// a single interface for all platforms. API is inspired by Hive. /// a single interface for all platforms. API is inspired by Hive.
class BoxCollection { class BoxCollection {
final Database _db; final Database _db;
final Set<String> boxNames; final Set<String> boxNames;
final DatabaseFactory? _factory;
BoxCollection(this._db, this.boxNames); BoxCollection(this._db, this.boxNames, this._factory);
static Future<BoxCollection> open( static Future<BoxCollection> open(
String name, String name,
Set<String> boxNames, { Set<String> boxNames, {
Object? sqfliteDatabase, Object? sqfliteDatabase,
DatabaseFactory? sqfliteFactory,
dynamic idbFactory, dynamic idbFactory,
}) async { }) async {
if (sqfliteDatabase is! Database) { if (sqfliteDatabase is! Database) {
@ -28,7 +30,7 @@ class BoxCollection {
batch.execute('CREATE INDEX IF NOT EXISTS k_index ON $name (k)'); batch.execute('CREATE INDEX IF NOT EXISTS k_index ON $name (k)');
} }
await batch.commit(noResult: true); await batch.commit(noResult: true);
return BoxCollection(sqfliteDatabase, boxNames); return BoxCollection(sqfliteDatabase, boxNames, sqfliteFactory);
} }
Box<V> openBox<V>(String name) { Box<V> openBox<V>(String name) {
@ -110,6 +112,9 @@ class BoxCollection {
); );
Future<void> close() => _db.close(); Future<void> close() => _db.close();
Future<void> delete() =>
(_factory ?? databaseFactory).deleteDatabase(_db.path);
} }
class Box<V> { class Box<V> {

View File

@ -15,6 +15,7 @@ void main() {
'testbox', 'testbox',
boxNames, boxNames,
sqfliteDatabase: db, sqfliteDatabase: db,
sqfliteFactory: databaseFactoryFfi,
); );
}); });
@ -76,5 +77,9 @@ void main() {
expect(await box.get('fluffy'), null); expect(await box.get('fluffy'), null);
expect(await box.get('loki'), null); expect(await box.get('loki'), null);
}); });
test('Box.delete', () async {
await collection.delete();
});
}); });
} }

View File

@ -472,6 +472,19 @@ void main() {
test('Close', () async { test('Close', () async {
await database.close(); await database.close();
}); });
test('Delete', () async {
final database = await getMatrixSdkDatabase(null);
await database.storeAccountData(
'm.test.data',
jsonEncode({'foo': 'bar'}),
);
await database.delete();
// Check if previously stored data is gone:
final reopenedDatabase = await getMatrixSdkDatabase(null);
final dump = await reopenedDatabase.getAccountData();
expect(dump.isEmpty, true);
});
}); });
} }

View File

@ -44,7 +44,11 @@ Future<HiveCollectionsDatabase> getHiveCollectionsDatabase(Client? c) async {
// ignore: deprecated_member_use_from_same_package // ignore: deprecated_member_use_from_same_package
Future<MatrixSdkDatabase> getMatrixSdkDatabase(Client? c) async { Future<MatrixSdkDatabase> getMatrixSdkDatabase(Client? c) async {
final database = await databaseFactoryFfi.openDatabase(':memory:'); final database = await databaseFactoryFfi.openDatabase(':memory:');
final db = MatrixSdkDatabase('unit_test.${c?.hashCode}', database: database); final db = MatrixSdkDatabase(
'unit_test.${c?.hashCode}',
database: database,
sqfliteFactory: databaseFactoryFfi,
);
await db.open(); await db.open();
return db; return db;
} }