diff --git a/lib/src/client.dart b/lib/src/client.dart index 1a40a2df..a69c2693 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -519,14 +519,13 @@ class Client extends MatrixApi { /// including all persistent data from the store. @override Future logoutAll() async { - try { - await super.logoutAll(); - } catch (e, s) { + final futures = []; + futures.add(super.logoutAll()); + futures.add(clear()); + await Future.wait(futures).catchError((e, s) { Logs().e('Logout all failed', e, s); - rethrow; - } finally { - await clear(); - } + throw e; + }); } /// Run any request and react on user interactive authentication flows here. @@ -816,6 +815,55 @@ class Client extends MatrixApi { } } + /// dumps the local database and exports it into a String. + /// + /// WARNING: never re-import the dump twice + /// + /// This can be useful to migrate a session from one device to a future one. + Future exportDump() async { + if (database != null) { + await abortSync(); + await dispose(closeDatabase: false); + + final export = await database!.exportDump(); + + await clear(); + return export; + } + return null; + } + + /// imports a dumped session + /// + /// WARNING: never re-import the dump twice + Future importDump(String export) async { + try { + // stopping sync loop and subscriptions while keeping DB open + await dispose(closeDatabase: false); + } finally { + _database ??= await databaseBuilder!.call(this); + + final success = await database!.importDump(export); + + if (success) { + // closing including DB + await dispose(); + + try { + bearerToken = null; + + await init( + waitForFirstSync: false, + waitUntilLoadCompletedLoaded: false, + ); + } catch (e) { + return false; + } + } + return success; + } + } + /// Uploads a new user avatar for this user. Leave file null to remove the /// current avatar. Future setAvatar(MatrixFile? file) async { diff --git a/lib/src/database/database_api.dart b/lib/src/database/database_api.dart index eff50d85..c8a65477 100644 --- a/lib/src/database/database_api.dart +++ b/lib/src/database/database_api.dart @@ -310,4 +310,8 @@ abstract class DatabaseApi { Future close(); Future transaction(Future Function() action); + + Future exportDump(); + + Future importDump(String export); } diff --git a/lib/src/database/fluffybox_database.dart b/lib/src/database/fluffybox_database.dart index 9be9190d..804dcdda 100644 --- a/lib/src/database/fluffybox_database.dart +++ b/lib/src/database/fluffybox_database.dart @@ -39,7 +39,7 @@ class FluffyBoxDatabase extends DatabaseApi { final String name; final String path; final HiveCipher? key; - late final BoxCollection _collection; + late BoxCollection _collection; late Box _clientBox; late Box _accountDataBox; late Box _roomsBox; @@ -1458,4 +1458,108 @@ class FluffyBoxDatabase extends DatabaseApi { if (raw == null) return null; return raw; } + + @override + Future exportDump() async { + final dataMap = { + _clientBoxName: await _clientBox.getAllValues(), + _accountDataBoxName: await _accountDataBox.getAllValues(), + _roomsBoxName: await _roomsBox.getAllValues(), + _roomStateBoxName: await _roomStateBox.getAllValues(), + _roomMembersBoxName: await _roomMembersBox.getAllValues(), + _toDeviceQueueBoxName: await _toDeviceQueueBox.getAllValues(), + _roomAccountDataBoxName: await _roomAccountDataBox.getAllValues(), + _inboundGroupSessionsBoxName: + await _inboundGroupSessionsBox.getAllValues(), + _outboundGroupSessionsBoxName: + await _outboundGroupSessionsBox.getAllValues(), + _olmSessionsBoxName: await _olmSessionsBox.getAllValues(), + _userDeviceKeysBoxName: await _userDeviceKeysBox.getAllValues(), + _userDeviceKeysOutdatedBoxName: + await _userDeviceKeysOutdatedBox.getAllValues(), + _userCrossSigningKeysBoxName: + await _userCrossSigningKeysBox.getAllValues(), + _ssssCacheBoxName: await _ssssCacheBox.getAllValues(), + _presencesBoxName: await _presencesBox.getAllValues(), + _timelineFragmentsBoxName: await _timelineFragmentsBox.getAllValues(), + _eventsBoxName: await _eventsBox.getAllValues(), + _seenDeviceIdsBoxName: await _seenDeviceIdsBox.getAllValues(), + _seenDeviceKeysBoxName: await _seenDeviceKeysBox.getAllValues(), + }; + final json = jsonEncode(dataMap); + await clear(); + return json; + } + + @override + Future importDump(String export) async { + try { + await clear(); + await open(); + final json = Map.from(jsonDecode(export)).cast(); + for (final key in json[_clientBoxName]!.keys) { + await _clientBox.put(key, json[_clientBoxName]![key]); + } + for (final key in json[_accountDataBoxName]!.keys) { + await _accountDataBox.put(key, json[_accountDataBoxName]![key]); + } + for (final key in json[_roomsBoxName]!.keys) { + await _roomsBox.put(key, json[_roomsBoxName]![key]); + } + for (final key in json[_roomStateBoxName]!.keys) { + await _roomStateBox.put(key, json[_roomStateBoxName]![key]); + } + for (final key in json[_roomMembersBoxName]!.keys) { + await _roomMembersBox.put(key, json[_roomMembersBoxName]![key]); + } + for (final key in json[_toDeviceQueueBoxName]!.keys) { + await _toDeviceQueueBox.put(key, json[_toDeviceQueueBoxName]![key]); + } + for (final key in json[_roomAccountDataBoxName]!.keys) { + await _roomAccountDataBox.put(key, json[_roomAccountDataBoxName]![key]); + } + for (final key in json[_inboundGroupSessionsBoxName]!.keys) { + await _inboundGroupSessionsBox.put( + key, json[_inboundGroupSessionsBoxName]![key]); + } + for (final key in json[_outboundGroupSessionsBoxName]!.keys) { + await _outboundGroupSessionsBox.put( + key, json[_outboundGroupSessionsBoxName]![key]); + } + for (final key in json[_olmSessionsBoxName]!.keys) { + await _olmSessionsBox.put(key, json[_olmSessionsBoxName]![key]); + } + for (final key in json[_userDeviceKeysBoxName]!.keys) { + await _userDeviceKeysBox.put(key, json[_userDeviceKeysBoxName]![key]); + } + for (final key in json[_userDeviceKeysOutdatedBoxName]!.keys) { + await _userDeviceKeysOutdatedBox.put( + key, json[_userDeviceKeysOutdatedBoxName]![key]); + } + for (final key in json[_userCrossSigningKeysBoxName]!.keys) { + await _userCrossSigningKeysBox.put( + key, json[_userCrossSigningKeysBoxName]![key]); + } + for (final key in json[_ssssCacheBoxName]!.keys) { + await _ssssCacheBox.put(key, json[_ssssCacheBoxName]![key]); + } + for (final key in json[_presencesBoxName]!.keys) { + await _presencesBox.put(key, json[_presencesBoxName]![key]); + } + for (final key in json[_timelineFragmentsBoxName]!.keys) { + await _timelineFragmentsBox.put( + key, json[_timelineFragmentsBoxName]![key]); + } + for (final key in json[_seenDeviceIdsBoxName]!.keys) { + await _seenDeviceIdsBox.put(key, json[_seenDeviceIdsBoxName]![key]); + } + for (final key in json[_seenDeviceKeysBoxName]!.keys) { + await _seenDeviceKeysBox.put(key, json[_seenDeviceKeysBoxName]![key]); + } + return true; + } catch (e, s) { + Logs().e('Database import error: ', e, s); + return false; + } + } } diff --git a/lib/src/database/hive_collections_database.dart b/lib/src/database/hive_collections_database.dart index b9af4683..8bf7e7ae 100644 --- a/lib/src/database/hive_collections_database.dart +++ b/lib/src/database/hive_collections_database.dart @@ -1460,6 +1460,110 @@ class HiveCollectionsDatabase extends DatabaseApi { if (raw == null) return null; return raw; } + + @override + Future exportDump() async { + final dataMap = { + _clientBoxName: await _clientBox.getAllValues(), + _accountDataBoxName: await _accountDataBox.getAllValues(), + _roomsBoxName: await _roomsBox.getAllValues(), + _roomStateBoxName: await _roomStateBox.getAllValues(), + _roomMembersBoxName: await _roomMembersBox.getAllValues(), + _toDeviceQueueBoxName: await _toDeviceQueueBox.getAllValues(), + _roomAccountDataBoxName: await _roomAccountDataBox.getAllValues(), + _inboundGroupSessionsBoxName: + await _inboundGroupSessionsBox.getAllValues(), + _outboundGroupSessionsBoxName: + await _outboundGroupSessionsBox.getAllValues(), + _olmSessionsBoxName: await _olmSessionsBox.getAllValues(), + _userDeviceKeysBoxName: await _userDeviceKeysBox.getAllValues(), + _userDeviceKeysOutdatedBoxName: + await _userDeviceKeysOutdatedBox.getAllValues(), + _userCrossSigningKeysBoxName: + await _userCrossSigningKeysBox.getAllValues(), + _ssssCacheBoxName: await _ssssCacheBox.getAllValues(), + _presencesBoxName: await _presencesBox.getAllValues(), + _timelineFragmentsBoxName: await _timelineFragmentsBox.getAllValues(), + _eventsBoxName: await _eventsBox.getAllValues(), + _seenDeviceIdsBoxName: await _seenDeviceIdsBox.getAllValues(), + _seenDeviceKeysBoxName: await _seenDeviceKeysBox.getAllValues(), + }; + final json = jsonEncode(dataMap); + await clear(); + return json; + } + + @override + Future importDump(String export) async { + try { + await clear(); + await open(); + final json = Map.from(jsonDecode(export)).cast(); + for (final key in json[_clientBoxName]!.keys) { + await _clientBox.put(key, json[_clientBoxName]![key]); + } + for (final key in json[_accountDataBoxName]!.keys) { + await _accountDataBox.put(key, json[_accountDataBoxName]![key]); + } + for (final key in json[_roomsBoxName]!.keys) { + await _roomsBox.put(key, json[_roomsBoxName]![key]); + } + for (final key in json[_roomStateBoxName]!.keys) { + await _roomStateBox.put(key, json[_roomStateBoxName]![key]); + } + for (final key in json[_roomMembersBoxName]!.keys) { + await _roomMembersBox.put(key, json[_roomMembersBoxName]![key]); + } + for (final key in json[_toDeviceQueueBoxName]!.keys) { + await _toDeviceQueueBox.put(key, json[_toDeviceQueueBoxName]![key]); + } + for (final key in json[_roomAccountDataBoxName]!.keys) { + await _roomAccountDataBox.put(key, json[_roomAccountDataBoxName]![key]); + } + for (final key in json[_inboundGroupSessionsBoxName]!.keys) { + await _inboundGroupSessionsBox.put( + key, json[_inboundGroupSessionsBoxName]![key]); + } + for (final key in json[_outboundGroupSessionsBoxName]!.keys) { + await _outboundGroupSessionsBox.put( + key, json[_outboundGroupSessionsBoxName]![key]); + } + for (final key in json[_olmSessionsBoxName]!.keys) { + await _olmSessionsBox.put(key, json[_olmSessionsBoxName]![key]); + } + for (final key in json[_userDeviceKeysBoxName]!.keys) { + await _userDeviceKeysBox.put(key, json[_userDeviceKeysBoxName]![key]); + } + for (final key in json[_userDeviceKeysOutdatedBoxName]!.keys) { + await _userDeviceKeysOutdatedBox.put( + key, json[_userDeviceKeysOutdatedBoxName]![key]); + } + for (final key in json[_userCrossSigningKeysBoxName]!.keys) { + await _userCrossSigningKeysBox.put( + key, json[_userCrossSigningKeysBoxName]![key]); + } + for (final key in json[_ssssCacheBoxName]!.keys) { + await _ssssCacheBox.put(key, json[_ssssCacheBoxName]![key]); + } + for (final key in json[_presencesBoxName]!.keys) { + await _presencesBox.put(key, json[_presencesBoxName]![key]); + } + for (final key in json[_timelineFragmentsBoxName]!.keys) { + await _timelineFragmentsBox.put( + key, json[_timelineFragmentsBoxName]![key]); + } + for (final key in json[_seenDeviceIdsBoxName]!.keys) { + await _seenDeviceIdsBox.put(key, json[_seenDeviceIdsBoxName]![key]); + } + for (final key in json[_seenDeviceKeysBoxName]!.keys) { + await _seenDeviceKeysBox.put(key, json[_seenDeviceKeysBoxName]![key]); + } + return true; + } catch (e, s) { + Logs().e('Database import error: ', e, s); + return false; + } + } } class TupleKey { diff --git a/lib/src/database/hive_database.dart b/lib/src/database/hive_database.dart index 29100931..d8b285c0 100644 --- a/lib/src/database/hive_database.dart +++ b/lib/src/database/hive_database.dart @@ -1405,6 +1405,18 @@ class FamedlySdkHiveDatabase extends DatabaseApi { if (raw == null) return null; return raw as String; } + + @override + Future exportDump() { + // see no need to implement this in a deprecated part + throw UnimplementedError(); + } + + @override + Future importDump(String export) { + // see no need to implement this in a deprecated part + throw UnimplementedError(); + } } dynamic _castValue(dynamic value) { diff --git a/test_driver/matrixsdk_test.dart b/test_driver/matrixsdk_test.dart index a5d252ad..72844e51 100644 --- a/test_driver/matrixsdk_test.dart +++ b/test_driver/matrixsdk_test.dart @@ -130,6 +130,10 @@ void test() async { .deviceKeys[testClientA.deviceID!]!.verified); assert(!testClientB.userDeviceKeys[TestUser.username]! .deviceKeys[testClientA.deviceID!]!.blocked); + await Future.wait([ + testClientA.updateUserDeviceKeys(), + testClientB.updateUserDeviceKeys(), + ]); await testClientA .userDeviceKeys[TestUser.username2]!.deviceKeys[testClientB.deviceID!]! .setVerified(true);