feat: db versioning and better logging

This commit is contained in:
Aliaksei Tratseuski 2024-06-26 01:54:56 +04:00
parent 78b026ed42
commit 85bc997776
6 changed files with 293 additions and 138 deletions

4
.vscode/launch.json vendored
View file

@ -31,7 +31,7 @@
]
},
{
"name": "debug (production)",
"name": "debug (production flavor)",
"request": "launch",
"type": "dart",
"args": [
@ -41,7 +41,7 @@
},
{
"name": "debug (nightly)",
"name": "debug (nightly flavor)",
"request": "launch",
"type": "dart",
"args": [

18
lib/config/config.dart Normal file
View file

@ -0,0 +1,18 @@
import 'package:flutter/foundation.dart';
/// internal app configuration
const config = InternalConfig(
shouldDebugPrint: kDebugMode,
);
class InternalConfig {
const InternalConfig({
required this.shouldDebugPrint,
});
final bool shouldDebugPrint;
// example of other possible fields
// final String appVersion;
//
}

View file

@ -1,7 +1,3 @@
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart';
import 'package:selfprivacy/logic/models/hive/backups_credential.dart';
@ -12,144 +8,210 @@ import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/hive/server_provider_credential.dart';
import 'package:selfprivacy/logic/models/hive/user.dart';
import 'package:selfprivacy/logic/models/hive/wizards_data/server_installation_wizard_data.dart';
import 'package:selfprivacy/utils/app_logger.dart';
import 'package:selfprivacy/utils/platform_adapter.dart';
import 'package:selfprivacy/utils/secure_storage.dart';
class HiveConfig {
static final log = const AppLogger(name: 'hive_config').log;
/// bump on schema changes
static const version = 2;
static Future<void> init() async {
final String? storagePath = PlatformAdapter.storagePath;
print('HiveConfig: Custom storage path: $storagePath');
log('set custom storage path to: "$storagePath"');
await Hive.initFlutter(storagePath);
Hive.registerAdapter(UserAdapter());
Hive.registerAdapter(ServerHostingDetailsAdapter());
Hive.registerAdapter(ServerDomainAdapter());
Hive.registerAdapter(BackupsCredentialAdapter());
Hive.registerAdapter(ServerProviderVolumeAdapter());
Hive.registerAdapter(BackblazeBucketAdapter());
Hive.registerAdapter(ServerProviderCredentialAdapter());
Hive.registerAdapter(DnsProviderCredentialAdapter());
Hive.registerAdapter(ServerAdapter());
Hive.registerAdapter(DnsProviderTypeAdapter());
Hive.registerAdapter(ServerProviderTypeAdapter());
Hive.registerAdapter(UserTypeAdapter());
Hive.registerAdapter(BackupsProviderTypeAdapter());
Hive.registerAdapter(ServerInstallationWizardDataAdapter());
await Hive.openBox(BNames.appSettingsBox);
registerAdapters();
await decryptBoxes();
await performMigrations();
}
static void registerAdapters() {
try {
final HiveAesCipher cipher = HiveAesCipher(
await getEncryptedKey(BNames.serverInstallationEncryptionKey),
Hive.registerAdapter(UserAdapter());
Hive.registerAdapter(ServerHostingDetailsAdapter());
Hive.registerAdapter(ServerDomainAdapter());
Hive.registerAdapter(BackupsCredentialAdapter());
Hive.registerAdapter(ServerProviderVolumeAdapter());
Hive.registerAdapter(BackblazeBucketAdapter());
Hive.registerAdapter(ServerProviderCredentialAdapter());
Hive.registerAdapter(DnsProviderCredentialAdapter());
Hive.registerAdapter(ServerAdapter());
Hive.registerAdapter(DnsProviderTypeAdapter());
Hive.registerAdapter(ServerProviderTypeAdapter());
Hive.registerAdapter(UserTypeAdapter());
Hive.registerAdapter(BackupsProviderTypeAdapter());
Hive.registerAdapter(ServerInstallationWizardDataAdapter());
log('successfully registered every adapter');
} catch (error, stackTrace) {
log(
'error registering adapters',
error: error,
stackTrace: stackTrace,
);
await Hive.openBox(BNames.serverInstallationBox,
encryptionCipher: cipher);
await Hive.openBox(BNames.resourcesBox, encryptionCipher: cipher);
await Hive.openBox(BNames.wizardDataBox, encryptionCipher: cipher);
final Box resourcesBox = Hive.box(BNames.resourcesBox);
if (resourcesBox.isEmpty) {
final Box serverInstallationBox =
Hive.box(BNames.serverInstallationBox);
final String? serverProviderKey =
serverInstallationBox.get(BNames.hetznerKey);
final String? serverLocation =
serverInstallationBox.get(BNames.serverLocation);
final String? dnsProviderKey =
serverInstallationBox.get(BNames.cloudFlareKey);
final BackupsCredential? backblazeCredential =
serverInstallationBox.get(BNames.backblazeCredential);
final ServerDomain? serverDomain =
serverInstallationBox.get(BNames.serverDomain);
final ServerHostingDetails? serverDetails =
serverInstallationBox.get(BNames.serverDetails);
final BackblazeBucket? backblazeBucket =
serverInstallationBox.get(BNames.backblazeBucket);
final String? serverType =
serverInstallationBox.get(BNames.serverTypeIdentifier);
final ServerProviderType? serverProvider =
serverInstallationBox.get(BNames.serverProvider);
final DnsProviderType? dnsProvider =
serverInstallationBox.get(BNames.dnsProvider);
if (serverProviderKey != null &&
(serverProvider != null ||
(serverDetails != null &&
serverDetails.provider != ServerProviderType.unknown))) {
final ServerProviderCredential serverProviderCredential =
ServerProviderCredential(
tokenId: null,
token: serverProviderKey,
provider: serverProvider ?? serverDetails!.provider,
associatedServerIds:
serverDetails != null ? [serverDetails.id] : [],
);
await resourcesBox
.put(BNames.serverProviderTokens, [serverProviderCredential]);
}
if (dnsProviderKey != null &&
(dnsProvider != null ||
(serverDomain != null &&
serverDomain.provider != DnsProviderType.unknown))) {
final DnsProviderCredential dnsProviderCredential =
DnsProviderCredential(
tokenId: null,
token: dnsProviderKey,
provider: dnsProvider ?? serverDomain!.provider,
associatedDomainNames:
serverDomain != null ? [serverDomain.domainName] : [],
);
await resourcesBox
.put(BNames.dnsProviderTokens, [dnsProviderCredential]);
}
if (backblazeCredential != null) {
await resourcesBox
.put(BNames.backupsProviderTokens, [backblazeCredential]);
}
if (backblazeBucket != null) {
await resourcesBox.put(BNames.backblazeBucket, backblazeBucket);
}
if (serverDetails != null && serverDomain != null) {
await resourcesBox.put(BNames.servers, [
Server(
domain: serverDomain,
hostingDetails: serverDetails.copyWith(
serverLocation: serverLocation,
serverType: serverType,
),
),
]);
}
}
} on PlatformException catch (e) {
print('HiveConfig: Error while opening boxes: $e');
rethrow;
}
}
static Future<Uint8List> getEncryptedKey(final String encKey) async {
const FlutterSecureStorage secureStorage = FlutterSecureStorage();
try {
final bool hasEncryptionKey =
await secureStorage.containsKey(key: encKey);
if (!hasEncryptionKey) {
final List<int> key = Hive.generateSecureKey();
await secureStorage.write(key: encKey, value: base64UrlEncode(key));
}
static Future<HiveAesCipher> getCipher() async {
List<int>? key = await SecureStorage.getKey();
if (key == null) {
key = Hive.generateSecureKey();
await SecureStorage.setKey(key);
}
return HiveAesCipher(key);
}
final String? string = await secureStorage.read(key: encKey);
return base64Url.decode(string!);
} on PlatformException catch (e) {
print('HiveConfig: Error while getting encryption key: $e');
static Future<void> decryptBoxes() async {
try {
// load encrypted boxes into memory
final HiveAesCipher cipher = await getCipher();
await Hive.openBox(
BNames.serverInstallationBox,
encryptionCipher: cipher,
);
await Hive.openBox(
BNames.resourcesBox,
encryptionCipher: cipher,
);
await Hive.openBox(
BNames.wizardDataBox,
encryptionCipher: cipher,
);
log('successfully decrypted boxes');
} catch (error, stackTrace) {
log(
'error initializing encrypted boxes',
error: error,
stackTrace: stackTrace,
);
rethrow;
}
}
// migrations
static Future<void> performMigrations() async {
try {
// perform migration check
final localSettingsBox = await Hive.openBox(BNames.appSettingsBox);
// if it is an initial app launch, we do not need to perform any migrations
final savedVersion = localSettingsBox.isEmpty
? version
// if box was initialized, but database version was not introduced in
// it yet, it means that we have initial value
: await localSettingsBox.get(BNames.databaseVersion, defaultValue: 1);
/// launch migrations based on version
if (savedVersion < version) {
if (savedVersion < 2) {
await migrateFrom1To2();
}
/// add new migrations here, like:
/// if (version < 3) {...}, etc.
/// update saved version after successfull migraions
await localSettingsBox.put(BNames.databaseVersion, version);
}
} catch (error, stackTrace) {
log(
'error running db migrations',
error: error,
stackTrace: stackTrace,
);
rethrow;
}
}
/// introduce and populate resourcesBox
static Future<void> migrateFrom1To2() async {
final Box resourcesBox = Hive.box(BNames.resourcesBox);
if (resourcesBox.isEmpty) {
final Box serverInstallationBox = Hive.box(BNames.serverInstallationBox);
final String? serverProviderKey =
serverInstallationBox.get(BNames.hetznerKey);
final String? serverLocation =
serverInstallationBox.get(BNames.serverLocation);
final String? dnsProviderKey =
serverInstallationBox.get(BNames.cloudFlareKey);
final BackupsCredential? backblazeCredential =
serverInstallationBox.get(BNames.backblazeCredential);
final ServerDomain? serverDomain =
serverInstallationBox.get(BNames.serverDomain);
final ServerHostingDetails? serverDetails =
serverInstallationBox.get(BNames.serverDetails);
final BackblazeBucket? backblazeBucket =
serverInstallationBox.get(BNames.backblazeBucket);
final String? serverType =
serverInstallationBox.get(BNames.serverTypeIdentifier);
final ServerProviderType? serverProvider =
serverInstallationBox.get(BNames.serverProvider);
final DnsProviderType? dnsProvider =
serverInstallationBox.get(BNames.dnsProvider);
if (serverProviderKey != null &&
(serverProvider != null ||
(serverDetails != null &&
serverDetails.provider != ServerProviderType.unknown))) {
final ServerProviderCredential serverProviderCredential =
ServerProviderCredential(
tokenId: null,
token: serverProviderKey,
provider: serverProvider ?? serverDetails!.provider,
associatedServerIds: serverDetails != null ? [serverDetails.id] : [],
);
await resourcesBox
.put(BNames.serverProviderTokens, [serverProviderCredential]);
}
if (dnsProviderKey != null &&
(dnsProvider != null ||
(serverDomain != null &&
serverDomain.provider != DnsProviderType.unknown))) {
final DnsProviderCredential dnsProviderCredential =
DnsProviderCredential(
tokenId: null,
token: dnsProviderKey,
provider: dnsProvider ?? serverDomain!.provider,
associatedDomainNames:
serverDomain != null ? [serverDomain.domainName] : [],
);
await resourcesBox
.put(BNames.dnsProviderTokens, [dnsProviderCredential]);
}
if (backblazeCredential != null) {
await resourcesBox
.put(BNames.backupsProviderTokens, [backblazeCredential]);
}
if (backblazeBucket != null) {
await resourcesBox.put(BNames.backblazeBucket, backblazeBucket);
}
if (serverDetails != null && serverDomain != null) {
await resourcesBox.put(BNames.servers, [
Server(
domain: serverDomain,
hostingDetails: serverDetails.copyWith(
serverLocation: serverLocation,
serverType: serverType,
),
),
]);
}
}
log('successfully migration of db from 1 to 2 version');
}
}
/// Mappings for the different boxes and their keys
@ -157,6 +219,9 @@ class BNames {
/// App settings box. Contains app settings like [darkThemeModeOn], [shouldShowOnboarding]
static String appSettingsBox = 'appSettings';
/// An integer with last saved version of the database
static String databaseVersion = 'databaseVersion';
/// A boolean field of [appSettingsBox] box.
static String darkThemeModeOn = 'isDarkModeOn';
@ -169,9 +234,6 @@ class BNames {
/// A string field
static String appLocale = 'appLocale';
/// Encryption key to decrypt [serverInstallationBox] box.
static String serverInstallationEncryptionKey = 'key';
/// Server installation box. Contains server details and provider tokens.
static String serverInstallationBox = 'appConfig';

View file

@ -1,19 +1,25 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
// import 'package:pretty_dio_logger/pretty_dio_logger.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/models/console_log.dart';
import 'package:selfprivacy/utils/app_logger.dart';
abstract class RestApiMap {
static final log = const AppLogger(name: 'rest_api_map').log;
Future<Dio> getClient({final BaseOptions? customOptions}) async {
final Dio dio = Dio(customOptions ?? (await options));
if (hasLogger) {
dio.interceptors.add(PrettyDioLogger());
// dio.interceptors.add(
// PrettyDioLogger(
// logPrint: (final object) => log('$object'),
// ),
// );
}
dio.interceptors.add(ConsoleInterceptor());
dio.httpClientAdapter = IOHttpClientAdapter(
@ -29,11 +35,7 @@ abstract class RestApiMap {
dio.interceptors.add(
InterceptorsWrapper(
onError: (final DioException e, final ErrorInterceptorHandler handler) {
print(e.requestOptions.path);
print(e.requestOptions.data);
print(e.message);
print(e.response);
log('got dio error', error: e);
return handler.next(e);
},
@ -103,7 +105,6 @@ class ConsoleInterceptor extends InterceptorsWrapper {
final ErrorInterceptorHandler handler,
) async {
final Response? response = err.response;
log(err.toString());
addConsoleLog(
ManualConsoleLog.warning(

26
lib/utils/app_logger.dart Normal file
View file

@ -0,0 +1,26 @@
import 'dart:developer' as developer;
import 'package:selfprivacy/config/config.dart';
class AppLogger {
const AppLogger({required this.name});
final String name;
// TODO: research other possible options, which could support both
// throttling and output formatting
void log(
final String message, {
final Object? error,
final StackTrace? stackTrace,
}) {
if (config.shouldDebugPrint) {
developer.log(
message,
error: error,
stackTrace: stackTrace,
time: DateTime.now(),
name: name,
);
}
}
}

View file

@ -0,0 +1,48 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:selfprivacy/utils/app_logger.dart';
class SecureStorage {
static final log = const AppLogger(name: 'secure_storage').log;
static const FlutterSecureStorage secureStorage = FlutterSecureStorage();
static String keyName = 'key';
static Future<Uint8List?> getKey() async {
try {
final bool hasEncryptionKey =
await secureStorage.containsKey(key: keyName);
if (!hasEncryptionKey) {
return null;
}
final String? string = await secureStorage.read(key: keyName);
log('successfully got encryption key: $string');
return base64Url.decode(string!);
} catch (error, stackTrace) {
log(
'error reading encryption key',
error: error,
stackTrace: stackTrace,
);
rethrow;
}
}
static Future<void> setKey(final List<int> key) async {
try {
final value = base64UrlEncode(key);
await secureStorage.write(key: keyName, value: value);
log('successfully saved encryption key: $value');
} catch (error, stackTrace) {
log(
'error saving new encryption key',
error: error,
stackTrace: stackTrace,
);
rethrow;
}
}
}