Merge branch 'soru/fix-store' into 'main'

fix: Multiple related store things

Closes #189

See merge request ChristianPauly/fluffychat-flutter!241
This commit is contained in:
Christian Pauly 2020-10-26 11:19:02 +00:00
commit 489de95689
8 changed files with 37 additions and 241 deletions

View file

@ -81,8 +81,7 @@ class MatrixState extends State<Matrix> {
void clean() async { void clean() async {
if (!kIsWeb) return; if (!kIsWeb) return;
final storage = await getLocalStorage(); await store.deleteItem(widget.clientName);
await storage.deleteItem(widget.clientName);
} }
void _initWithStore() async { void _initWithStore() async {

View file

@ -175,7 +175,7 @@ class ThemeSwitcherWidgetState extends State<ThemeSwitcherWidget> {
BuildContext context; BuildContext context;
Future loadSelection(MatrixState matrix) async { Future loadSelection(MatrixState matrix) async {
String item = await matrix.store.getItem('theme') ?? 'system'; var item = await matrix.store.getItem('theme') ?? 'system';
selectedTheme = Themes.values.firstWhere( selectedTheme = Themes.values.firstWhere(
(e) => e.toString() == 'Themes.' + item, (e) => e.toString() == 'Themes.' + item,
orElse: () => Themes.system); orElse: () => Themes.system);

View file

@ -21,8 +21,8 @@ final sentry = SentryClient(dsn: '8591d0d863b646feb4f3dda7e5dcab38');
void captureException(error, stackTrace) async { void captureException(error, stackTrace) async {
debugPrint(error.toString()); debugPrint(error.toString());
debugPrint(stackTrace.toString()); debugPrint(stackTrace.toString());
final storage = await getLocalStorage(); final storage = Store();
if (storage.getItem('sentry') == true) { if (await storage.getItem('sentry') == 'true') {
await sentry.captureException( await sentry.captureException(
exception: error, exception: error,
stackTrace: stackTrace, stackTrace: stackTrace,

View file

@ -1,27 +1,13 @@
import 'dart:convert';
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/platform_infos.dart';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:localstorage/localstorage.dart'; import 'package:localstorage/localstorage.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'dart:async'; import 'dart:async';
import 'dart:core'; import 'dart:core';
import './database/shared.dart'; import './database/shared.dart';
import 'package:olm/olm.dart' as olm; // needed for migration
import 'package:random_string/random_string.dart'; import 'package:random_string/random_string.dart';
Future<LocalStorage> getLocalStorage() async {
final directory = PlatformInfos.isBetaDesktop
? await getApplicationSupportDirectory()
: (PlatformInfos.isWeb ? null : await getApplicationDocumentsDirectory());
final localStorage = LocalStorage('LocalStorage', directory?.path);
await localStorage.ready;
return localStorage;
}
Future<Database> getDatabase(Client client) async { Future<Database> getDatabase(Client client) async {
while (_generateDatabaseLock) { while (_generateDatabaseLock) {
await Future.delayed(Duration(milliseconds: 50)); await Future.delayed(Duration(milliseconds: 50));
@ -31,9 +17,9 @@ Future<Database> getDatabase(Client client) async {
if (_db != null) return _db; if (_db != null) return _db;
final store = Store(); final store = Store();
var password = await store.getItem('database-password'); var password = await store.getItem('database-password');
var needMigration = false; var newPassword = false;
if (password == null || password.isEmpty) { if (password == null || password.isEmpty) {
needMigration = true; newPassword = true;
password = randomString(255); password = randomString(255);
} }
_db = await constructDb( _db = await constructDb(
@ -41,11 +27,7 @@ Future<Database> getDatabase(Client client) async {
filename: 'moor.sqlite', filename: 'moor.sqlite',
password: password, password: password,
); );
// Check if database is open: if (newPassword) {
debugPrint((await _db.customSelect('SELECT 1').get()).toString());
if (needMigration) {
debugPrint('[Moor] Start migration');
await migrate(client.clientName, _db, store);
await store.setItem('database-password', password); await store.setItem('database-password', password);
} }
return _db; return _db;
@ -57,239 +39,54 @@ Future<Database> getDatabase(Client client) async {
Database _db; Database _db;
bool _generateDatabaseLock = false; bool _generateDatabaseLock = false;
Future<void> migrate(String clientName, Database db, Store store) async {
debugPrint('[Store] attempting old migration to moor...');
final oldKeys = await store.getAllItems();
if (oldKeys == null || oldKeys.isEmpty) {
debugPrint('[Store] empty store!');
return; // we are done!
}
final credentialsStr = oldKeys[clientName];
if (credentialsStr == null || credentialsStr.isEmpty) {
debugPrint('[Store] no credentials found!');
return; // no credentials
}
final Map<String, dynamic> credentials = json.decode(credentialsStr);
if (!credentials.containsKey('homeserver') ||
!credentials.containsKey('token') ||
!credentials.containsKey('userID')) {
debugPrint('[Store] invalid credentials!');
return; // invalid old store, we are done, too!
}
var clientId = 0;
final oldClient = await db.getClient(clientName);
if (oldClient == null) {
clientId = await db.insertClient(
clientName,
credentials['homeserver'],
credentials['token'],
credentials['userID'],
credentials['deviceID'],
credentials['deviceName'],
null,
credentials['olmAccount'],
);
} else {
clientId = oldClient.clientId;
await db.updateClient(
credentials['homeserver'],
credentials['token'],
credentials['userID'],
credentials['deviceID'],
credentials['deviceName'],
null,
credentials['olmAccount'],
clientId,
);
}
await db.clearCache(clientId);
debugPrint('[Store] Inserted/updated client, clientId = ${clientId}');
await db.transaction(() async {
// alright, we stored / updated the client and have the account ID, time to import everything else!
// user_device_keys and user_device_keys_key
debugPrint('[Store] Migrating user device keys...');
final deviceKeysListString = oldKeys['${clientName}.user_device_keys'];
if (deviceKeysListString != null && deviceKeysListString.isNotEmpty) {
Map<String, dynamic> rawUserDeviceKeys =
json.decode(deviceKeysListString);
for (final entry in rawUserDeviceKeys.entries) {
final map = entry.value;
await db.storeUserDeviceKeysInfo(
clientId, map['user_id'], map['outdated']);
for (final rawKey in map['device_keys'].entries) {
final jsonVaue = rawKey.value;
await db.storeUserDeviceKey(
clientId,
jsonVaue['user_id'],
jsonVaue['device_id'],
json.encode(jsonVaue),
jsonVaue['verified'],
jsonVaue['blocked']);
}
}
}
for (final entry in oldKeys.entries) {
final key = entry.key;
final value = entry.value;
if (value == null || value.isEmpty) {
continue;
}
// olm_sessions
final olmSessionsMatch =
RegExp(r'^\/clients\/([^\/]+)\/olm-sessions$').firstMatch(key);
if (olmSessionsMatch != null) {
if (olmSessionsMatch[1] != credentials['deviceID']) {
continue;
}
debugPrint('[Store] migrating olm sessions...');
final identityKey = json.decode(value);
for (final olmKey in identityKey.entries) {
final identKey = olmKey.key;
final sessions = olmKey.value;
for (final pickle in sessions) {
var sess = olm.Session();
sess.unpickle(credentials['userID'], pickle);
await db.storeOlmSession(
clientId, identKey, sess.session_id(), pickle, null);
sess?.free();
}
}
}
// outbound_group_sessions
final outboundGroupSessionsMatch = RegExp(
r'^\/clients\/([^\/]+)\/rooms\/([^\/]+)\/outbound_group_session$')
.firstMatch(key);
if (outboundGroupSessionsMatch != null) {
if (outboundGroupSessionsMatch[1] != credentials['deviceID']) {
continue;
}
final pickle = value;
final roomId = outboundGroupSessionsMatch[2];
debugPrint(
'[Store] Migrating outbound group sessions for room ${roomId}...');
final devicesString = oldKeys[
'/clients/${outboundGroupSessionsMatch[1]}/rooms/${roomId}/outbound_group_session_devices'];
var devices = <String>[];
if (devicesString != null) {
devices = List<String>.from(json.decode(devicesString));
}
await db.storeOutboundGroupSession(
clientId,
roomId,
pickle,
json.encode(devices),
DateTime.now().millisecondsSinceEpoch,
0,
);
}
// session_keys
final sessionKeysMatch =
RegExp(r'^\/clients\/([^\/]+)\/rooms\/([^\/]+)\/session_keys$')
.firstMatch(key);
if (sessionKeysMatch != null) {
if (sessionKeysMatch[1] != credentials['deviceID']) {
continue;
}
final roomId = sessionKeysMatch[2];
debugPrint('[Store] Migrating session keys for room ${roomId}...');
final map = json.decode(value);
for (final entry in map.entries) {
await db.storeInboundGroupSession(
clientId,
roomId,
entry.key,
entry.value['inboundGroupSession'],
json.encode(entry.value['content']),
json.encode(entry.value['indexes']),
null,
null);
}
}
}
});
}
// see https://github.com/mogol/flutter_secure_storage/issues/161#issuecomment-704578453
class AsyncMutex {
Completer<void> _completer;
Future<void> lock() async {
while (_completer != null) {
await _completer.future;
}
_completer = Completer<void>();
}
void unlock() {
assert(_completer != null);
final completer = _completer;
_completer = null;
completer.complete();
}
}
class Store { class Store {
final LocalStorage storage; LocalStorage storage;
final FlutterSecureStorage secureStorage; final FlutterSecureStorage secureStorage;
static final _mutex = AsyncMutex();
Store() Store()
: storage = LocalStorage('LocalStorage'), : secureStorage = PlatformInfos.isMobile ? FlutterSecureStorage() : null;
secureStorage = PlatformInfos.isMobile ? FlutterSecureStorage() : null;
Future<dynamic> getItem(String key) async { Future<void> _setupLocalStorage() async {
if (!PlatformInfos.isMobile) { if (storage == null) {
final directory = PlatformInfos.isBetaDesktop
? await getApplicationSupportDirectory()
: (PlatformInfos.isWeb
? null
: await getApplicationDocumentsDirectory());
storage = LocalStorage('LocalStorage', directory?.path);
await storage.ready; await storage.ready;
}
}
Future<String> getItem(String key) async {
if (!PlatformInfos.isMobile) {
await _setupLocalStorage();
try { try {
return await storage.getItem(key); return await storage.getItem(key).toString();
} catch (_) { } catch (_) {
return null; return null;
} }
} }
try { try {
await _mutex.lock();
return await secureStorage.read(key: key); return await secureStorage.read(key: key);
} catch (_) { } catch (_) {
return null; return null;
} finally {
_mutex.unlock();
} }
} }
Future<void> setItem(String key, String value) async { Future<void> setItem(String key, String value) async {
if (!PlatformInfos.isMobile) { if (!PlatformInfos.isMobile) {
await storage.ready; await _setupLocalStorage();
return await storage.setItem(key, value); return await storage.setItem(key, value);
} }
if (value == null) {
return await secureStorage.delete(key: key);
} else {
try {
await _mutex.lock();
return await secureStorage.write(key: key, value: value); return await secureStorage.write(key: key, value: value);
} finally {
_mutex.unlock();
}
}
} }
Future<Map<String, dynamic>> getAllItems() async { Future<void> deleteItem(String key) async {
if (!PlatformInfos.isMobile) { if (!PlatformInfos.isMobile) {
try { await _setupLocalStorage();
final rawStorage = await getLocalstorage('LocalStorage'); return await storage.deleteItem(key);
return json.decode(rawStorage); }
} catch (_) { return await secureStorage.delete(key: key);
return {};
}
}
try {
await _mutex.lock();
return await secureStorage.readAll();
} catch (_) {
return {};
} finally {
_mutex.unlock();
}
} }
} }

View file

@ -13,14 +13,14 @@ abstract class SentryController {
confirmText: L10n.of(context).ok, confirmText: L10n.of(context).ok,
cancelText: L10n.of(context).no, cancelText: L10n.of(context).no,
); );
final storage = await getLocalStorage(); final storage = Store();
await storage.setItem('sentry', enableSentry); await storage.setItem('sentry', enableSentry.toString());
BotToast.showText(text: L10n.of(context).changesHaveBeenSaved); BotToast.showText(text: L10n.of(context).changesHaveBeenSaved);
return; return;
} }
static Future<bool> getSentryStatus() async { static Future<bool> getSentryStatus() async {
final storage = await getLocalStorage(); final storage = Store();
return storage.getItem('sentry') as bool; return await storage.getItem('sentry') == 'true';
} }
} }

View file

@ -185,7 +185,7 @@ class _SettingsState extends State<Settings> {
void deleteWallpaperAction(BuildContext context) async { void deleteWallpaperAction(BuildContext context) async {
Matrix.of(context).wallpaper = null; Matrix.of(context).wallpaper = null;
await Matrix.of(context).store.setItem('chat.fluffy.wallpaper', null); await Matrix.of(context).store.deleteItem('chat.fluffy.wallpaper');
setState(() => null); setState(() => null);
} }

View file

@ -362,7 +362,7 @@ packages:
name: flutter_secure_storage name: flutter_secure_storage
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.3.4" version: "3.3.5"
flutter_slidable: flutter_slidable:
dependency: "direct main" dependency: "direct main"
description: description:

View file

@ -41,7 +41,7 @@ dependencies:
path_provider: ^1.5.1 path_provider: ^1.5.1
webview_flutter: ^0.3.19+9 webview_flutter: ^0.3.19+9
share: ^0.6.3+5 share: ^0.6.3+5
flutter_secure_storage: ^3.3.4 flutter_secure_storage: ^3.3.5
http: ^0.12.0+4 http: ^0.12.0+4
universal_html: ^1.1.12 universal_html: ^1.1.12
receive_sharing_intent: ^1.3.3 receive_sharing_intent: ^1.3.3