/*
* Famedly Matrix SDK
* Copyright (C) 2020, 2021 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
import 'dart:async';
import 'dart:convert';
import 'package:matrix/encryption/utils/olm_session.dart';
import 'package:matrix/encryption/utils/outbound_group_session.dart';
import 'package:matrix/encryption/utils/ssss_cache.dart';
import 'package:matrix/encryption/utils/stored_inbound_group_session.dart';
import 'package:matrix/src/utils/QueuedToDeviceEvent.dart';
import 'package:moor/moor.dart';
import '../../matrix.dart' as sdk;
import 'package:matrix_api_lite/matrix_api_lite.dart' as api;
import '../client.dart';
import '../room.dart';
import 'database_api.dart';
part 'database.g.dart';
extension MigratorExtension on Migrator {
Future createIndexIfNotExists(Index index) async {
try {
await createIndex(index);
} catch (err) {
if (!err.toString().toLowerCase().contains('already exists')) {
rethrow;
}
}
}
Future createTableIfNotExists(TableInfo table) async {
try {
await createTable(table);
} catch (err) {
if (!err.toString().toLowerCase().contains('already exists')) {
rethrow;
}
}
}
Future addColumnIfNotExists(
TableInfo table, GeneratedColumn column) async {
try {
await addColumn(table, column);
} catch (err) {
if (!err.toString().toLowerCase().contains('duplicate column name')) {
rethrow;
}
}
}
}
@UseMoor(
include: {'database.moor'},
)
class Database extends _$Database implements DatabaseApi {
Database(QueryExecutor e) : super(e);
Database.connect(DatabaseConnection connection) : super.connect(connection);
@override
bool get supportsFileStoring => true;
@override
int get schemaVersion => 12;
@override
int get maxFileSize => 1 * 1024 * 1024;
/// Update errors are coming here.
final StreamController onError = StreamController.broadcast();
@override
MigrationStrategy get migration => MigrationStrategy(
onCreate: (Migrator m) async {
try {
await m.createAll();
} catch (e, s) {
api.Logs().e('Create all failed in database migrator', e, s);
onError.add(SdkError(exception: e, stackTrace: s));
rethrow;
}
},
onUpgrade: (Migrator m, int from, int to) async {
try {
// this appears to be only called once, so multiple consecutive upgrades have to be handled appropriately in here
if (from == 1) {
await m.createIndexIfNotExists(userDeviceKeysIndex);
await m.createIndexIfNotExists(userDeviceKeysKeyIndex);
await m.createIndexIfNotExists(olmSessionsIndex);
await m.createIndexIfNotExists(outboundGroupSessionsIndex);
await m.createIndexIfNotExists(inboundGroupSessionsIndex);
await m.createIndexIfNotExists(roomsIndex);
await m.createIndexIfNotExists(eventsIndex);
await m.createIndexIfNotExists(roomStatesIndex);
await m.createIndexIfNotExists(accountDataIndex);
await m.createIndexIfNotExists(roomAccountDataIndex);
await m.createIndexIfNotExists(presencesIndex);
from++;
}
if (from == 2) {
await m.deleteTable('outbound_group_sessions');
await m.createTable(outboundGroupSessions);
from++;
}
if (from == 3) {
await m.createTableIfNotExists(userCrossSigningKeys);
await m.createTableIfNotExists(ssssCache);
// mark all keys as outdated so that the cross signing keys will be fetched
await customStatement(
'UPDATE user_device_keys SET outdated = true');
from++;
}
if (from == 4) {
await m.addColumnIfNotExists(
olmSessions, olmSessions.lastReceived);
from++;
}
if (from == 5) {
await m.addColumnIfNotExists(
inboundGroupSessions, inboundGroupSessions.uploaded);
await m.addColumnIfNotExists(
inboundGroupSessions, inboundGroupSessions.senderKey);
await m.addColumnIfNotExists(
inboundGroupSessions, inboundGroupSessions.senderClaimedKeys);
from++;
}
if (from == 6) {
// DATETIME was internally an int, so we should be able to re-use the
// olm_sessions table.
await m.deleteTable('outbound_group_sessions');
await m.createTable(outboundGroupSessions);
await m.deleteTable('events');
await m.createTable(events);
await m.deleteTable('room_states');
await m.createTable(roomStates);
await m.deleteTable('files');
await m.createTable(files);
// and now clear cache
await delete(presences).go();
await delete(roomAccountData).go();
await delete(accountData).go();
await delete(roomStates).go();
await delete(events).go();
await delete(rooms).go();
await delete(outboundGroupSessions).go();
await customStatement('UPDATE clients SET prev_batch = null');
from++;
}
if (from == 7) {
await m.addColumnIfNotExists(
inboundGroupSessions, inboundGroupSessions.allowedAtIndex);
from++;
}
if (from == 8) {
await m.addColumnIfNotExists(
userDeviceKeysKey, userDeviceKeysKey.lastActive);
from++;
}
if (from == 9) {
await m.addColumnIfNotExists(
userDeviceKeysKey, userDeviceKeysKey.lastSentMessage);
await m.createIndexIfNotExists(olmSessionsIdentityIndex);
from++;
}
if (from == 10) {
await m.createTableIfNotExists(toDeviceQueue);
await m.createIndexIfNotExists(toDeviceQueueIndex);
from++;
}
if (from == 11) {
await m.addColumnIfNotExists(clients, clients.syncFilterId);
from++;
}
} catch (e, s) {
api.Logs().e('Database migration failed', e, s);
onError.add(SdkError(exception: e, stackTrace: s));
rethrow;
}
},
beforeOpen: (_) async {
try {
if (executor.dialect == SqlDialect.sqlite) {
final ret = await customSelect('PRAGMA journal_mode=WAL').get();
if (ret.isNotEmpty) {
api.Logs().v('[Moor] Switched database to mode ' +
ret.first.data['journal_mode'].toString());
}
}
} catch (e, s) {
api.Logs().e('Database before open failed', e, s);
onError.add(SdkError(exception: e, stackTrace: s));
rethrow;
}
},
);
@override
Future