From 82b8e1b6f020f22efe6b1712c634d5d74625f288 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Fri, 17 Nov 2023 21:30:09 +0330 Subject: [PATCH] Add geo assets settings --- assets/translations/strings_en.i18n.json | 14 ++ assets/translations/strings_fa.i18n.json | 14 ++ assets/translations/strings_ru.i18n.json | 14 ++ assets/translations/strings_zh.i18n.json | 14 ++ lib/core/router/routes/desktop_routes.dart | 20 +++ lib/core/router/routes/mobile_routes.dart | 22 +++ lib/data/data_providers.dart | 43 ++++- lib/data/local/dao/dao.dart | 1 + lib/data/local/dao/geo_assets_dao.dart | 36 ++++ lib/data/local/data_mappers.dart | 27 +++ lib/data/local/database.dart | 23 ++- lib/data/local/schema_versions.dart | 95 ++++++++++ lib/data/local/schemas/drift_schema_v3.json | 1 + lib/data/local/tables.dart | 20 +++ lib/data/repository/config_options_store.dart | 11 -- lib/data/repository/core_facade_impl.dart | 12 +- .../repository/geo_assets_repository.dart | 140 +++++++++++++++ lib/domain/rules/geo_asset.dart | 51 ++++++ lib/domain/rules/geo_asset_failure.dart | 39 ++++ lib/domain/rules/geo_assets_repository.dart | 12 ++ lib/domain/singbox/config_options.dart | 2 + .../settings/geo_assets/geo_asset_tile.dart | 116 ++++++++++++ .../geo_assets/geo_assets_notifier.dart | 28 +++ .../settings/geo_assets/geo_assets_page.dart | 35 ++++ .../widgets/advanced_setting_tiles.dart | 7 + lib/services/files_editor_service.dart | 36 +++- libcore | 2 +- .../local/generated_migrations/schema.dart | 5 +- .../local/generated_migrations/schema_v3.dart | 168 ++++++++++++++++++ test/data/local/migrations_test.dart | 24 ++- 30 files changed, 1003 insertions(+), 29 deletions(-) create mode 100644 lib/data/local/dao/geo_assets_dao.dart create mode 100644 lib/data/local/schemas/drift_schema_v3.json create mode 100644 lib/data/repository/geo_assets_repository.dart create mode 100644 lib/domain/rules/geo_asset.dart create mode 100644 lib/domain/rules/geo_asset_failure.dart create mode 100644 lib/domain/rules/geo_assets_repository.dart create mode 100644 lib/features/settings/geo_assets/geo_asset_tile.dart create mode 100644 lib/features/settings/geo_assets/geo_assets_notifier.dart create mode 100644 lib/features/settings/geo_assets/geo_assets_page.dart create mode 100644 test/data/local/generated_migrations/schema_v3.dart diff --git a/assets/translations/strings_en.i18n.json b/assets/translations/strings_en.i18n.json index cae1904b..98696753 100644 --- a/assets/translations/strings_en.i18n.json +++ b/assets/translations/strings_en.i18n.json @@ -217,6 +217,15 @@ "enableFakeDns": "Enable Fake DNS", "bypassLan": "Bypass Lan", "strictRoute": "Strict Route" + }, + "geoAssets": { + "pageTitle": "Routing Assets", + "version": "Version ${version}", + "fileMissing": "File Missing", + "update": "Update", + "download": "Download", + "failureMsg": "Failed to update asset", + "successMsg": "Successfully updated asset" } }, "about": { @@ -283,6 +292,11 @@ "badResponse": "Bad response", "connectionError": "Connection error", "badCertificate": "Bad certificate" + }, + "geoAssets": { + "unexpected": "Unexpected Error", + "notUpdate": "No Update Available", + "activeNotFound": "Active Geo Asset Not Found" } }, "play": { diff --git a/assets/translations/strings_fa.i18n.json b/assets/translations/strings_fa.i18n.json index 48b86072..426bc200 100644 --- a/assets/translations/strings_fa.i18n.json +++ b/assets/translations/strings_fa.i18n.json @@ -217,6 +217,15 @@ "enableFakeDns": "Enable Fake DNS", "bypassLan": "Bypass Lan", "strictRoute": "Strict Route" + }, + "geoAssets": { + "pageTitle": "فایل‌های مسیریابی", + "version": "نسخه ${version}", + "fileMissing": "فایل موجود نیست", + "update": "به روز رسانی", + "download": "دانلود", + "failureMsg": "دارایی به روز نشد", + "successMsg": "دارایی با موفقیت به روز شد" } }, "about": { @@ -283,6 +292,11 @@ "badResponse": "پاسخ نامعتبر", "connectionError": "خطای اتصال", "badCertificate": "خطای اعتبار سنجی" + }, + "geoAssets": { + "unexpected": "خطای غیرمنتظره", + "notUpdate": "به روز رسانی موجود نیست", + "activeNotFound": "Active Geo Asset یافت نشد" } }, "play": { diff --git a/assets/translations/strings_ru.i18n.json b/assets/translations/strings_ru.i18n.json index 35e085f6..b30959d6 100644 --- a/assets/translations/strings_ru.i18n.json +++ b/assets/translations/strings_ru.i18n.json @@ -217,6 +217,15 @@ "enableFakeDns": "Использовать поддельную DNS", "bypassLan": "Обход локальной сети", "strictRoute": "Строгая маршрутизация" + }, + "geoAssets": { + "pageTitle": "Активы маршрутизации", + "version": "Версия ${version}", + "fileMissing": "Файл отсутствует", + "update": "Обновлять", + "download": "Скачать", + "failureMsg": "Не удалось обновить объект.", + "successMsg": "Объект успешно обновлен." } }, "about": { @@ -283,6 +292,11 @@ "badResponse": "Неправильный ответ", "connectionError": "Ошибка подключения", "badCertificate": "Неправильный сертификат" + }, + "geoAssets": { + "unexpected": "Неожиданная ошибка", + "notUpdate": "Нет доступных обновлений", + "activeNotFound": "Активный географический актив не найден" } }, "play": { diff --git a/assets/translations/strings_zh.i18n.json b/assets/translations/strings_zh.i18n.json index 8a37edce..30ba3d30 100644 --- a/assets/translations/strings_zh.i18n.json +++ b/assets/translations/strings_zh.i18n.json @@ -217,6 +217,15 @@ "enableFakeDns": "启用 Fake DNS", "bypassLan": "绕过局域网", "strictRoute": "严格路由" + }, + "geoAssets": { + "pageTitle": "路由资产", + "version": "版本${version}", + "fileMissing": "文件丢失", + "update": "更新", + "download": "下载", + "failureMsg": "更新资产失败", + "successMsg": "已成功更新资产" } }, "about": { @@ -283,6 +292,11 @@ "badResponse": "错误响应", "connectionError": "连接错误", "badCertificate": "证书无效" + }, + "geoAssets": { + "unexpected": "意外的错误", + "notUpdate": "无可用更新", + "activeNotFound": "未找到活动地理资产" } }, "play": { diff --git a/lib/core/router/routes/desktop_routes.dart b/lib/core/router/routes/desktop_routes.dart index 20c1f87d..186f7ddd 100644 --- a/lib/core/router/routes/desktop_routes.dart +++ b/lib/core/router/routes/desktop_routes.dart @@ -4,6 +4,7 @@ import 'package:hiddify/core/router/routes/shared_routes.dart'; import 'package:hiddify/features/about/view/view.dart'; import 'package:hiddify/features/common/adaptive_root_scaffold.dart'; import 'package:hiddify/features/logs/view/view.dart'; +import 'package:hiddify/features/settings/geo_assets/geo_assets_page.dart'; import 'package:hiddify/features/settings/view/view.dart'; part 'desktop_routes.g.dart'; @@ -48,6 +49,10 @@ part 'desktop_routes.g.dart'; path: ConfigOptionsRoute.path, name: ConfigOptionsRoute.name, ), + TypedGoRoute( + path: GeoAssetsRoute.path, + name: GeoAssetsRoute.name, + ), ], ), TypedGoRoute( @@ -102,6 +107,21 @@ class ConfigOptionsRoute extends GoRouteData { } } +class GeoAssetsRoute extends GoRouteData { + const GeoAssetsRoute(); + static const path = 'routing-assets'; + static const name = 'Routing Assets'; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return const MaterialPage( + fullscreenDialog: true, + name: name, + child: GeoAssetsPage(), + ); + } +} + class AboutRoute extends GoRouteData { const AboutRoute(); static const path = '/about'; diff --git a/lib/core/router/routes/mobile_routes.dart b/lib/core/router/routes/mobile_routes.dart index 8cfd3ca3..79c28024 100644 --- a/lib/core/router/routes/mobile_routes.dart +++ b/lib/core/router/routes/mobile_routes.dart @@ -5,6 +5,7 @@ import 'package:hiddify/core/router/routes/shared_routes.dart'; import 'package:hiddify/features/about/view/view.dart'; import 'package:hiddify/features/common/adaptive_root_scaffold.dart'; import 'package:hiddify/features/logs/view/view.dart'; +import 'package:hiddify/features/settings/geo_assets/geo_assets_page.dart'; import 'package:hiddify/features/settings/view/view.dart'; part 'mobile_routes.g.dart'; @@ -47,6 +48,10 @@ part 'mobile_routes.g.dart'; path: PerAppProxyRoute.path, name: PerAppProxyRoute.name, ), + TypedGoRoute( + path: GeoAssetsRoute.path, + name: GeoAssetsRoute.name, + ), ], ), TypedGoRoute( @@ -138,6 +143,23 @@ class PerAppProxyRoute extends GoRouteData { } } +class GeoAssetsRoute extends GoRouteData { + const GeoAssetsRoute(); + static const path = 'routing-assets'; + static const name = 'Routing Assets'; + + static final GlobalKey $parentNavigatorKey = rootNavigatorKey; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return const MaterialPage( + fullscreenDialog: true, + name: name, + child: GeoAssetsPage(), + ); + } +} + class AboutRoute extends GoRouteData { const AboutRoute(); static const path = 'about'; diff --git a/lib/data/data_providers.dart b/lib/data/data_providers.dart index e4ac609d..f1c47452 100644 --- a/lib/data/data_providers.dart +++ b/lib/data/data_providers.dart @@ -8,11 +8,14 @@ import 'package:hiddify/data/local/dao/dao.dart'; import 'package:hiddify/data/local/database.dart'; import 'package:hiddify/data/repository/app_repository_impl.dart'; import 'package:hiddify/data/repository/config_options_store.dart'; +import 'package:hiddify/data/repository/geo_assets_repository.dart'; import 'package:hiddify/data/repository/repository.dart'; import 'package:hiddify/domain/app/app.dart'; import 'package:hiddify/domain/constants.dart'; import 'package:hiddify/domain/core_facade.dart'; import 'package:hiddify/domain/profiles/profiles.dart'; +import 'package:hiddify/domain/rules/geo_assets_repository.dart'; +import 'package:hiddify/domain/singbox/singbox.dart'; import 'package:hiddify/services/service_providers.dart'; import 'package:native_dio_adapter/native_dio_adapter.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -67,6 +70,44 @@ AppRepository appRepository(AppRepositoryRef ref) => @Riverpod(keepAlive: true) ClashApi clashApi(ClashApiRef ref) => ClashApi(Defaults.clashApiPort); +@Riverpod(keepAlive: true) +GeoAssetsDao geoAssetsDao(GeoAssetsDaoRef ref) => GeoAssetsDao( + ref.watch(appDatabaseProvider), + ); + +@Riverpod(keepAlive: true) +GeoAssetsRepository geoAssetsRepository(GeoAssetsRepositoryRef ref) { + return GeoAssetsRepositoryImpl( + geoAssetsDao: ref.watch(geoAssetsDaoProvider), + dio: ref.watch(dioProvider), + filesEditor: ref.watch(filesEditorServiceProvider), + ); +} + +@riverpod +Future configOptions(ConfigOptionsRef ref) async { + final geoAssets = await ref + .watch(geoAssetsRepositoryProvider) + .getActivePair() + .getOrElse((l) => throw l) + .run(); + final filesEditor = ref.watch(filesEditorServiceProvider); + + final serviceMode = ref.watch(serviceModeStoreProvider); + return ref.watch(configPreferencesProvider).copyWith( + enableTun: serviceMode == ServiceMode.tun, + setSystemProxy: serviceMode == ServiceMode.systemProxy, + geoipPath: filesEditor.geoAssetRelativePath( + geoAssets.geoip.providerName, + geoAssets.geoip.fileName, + ), + geositePath: filesEditor.geoAssetRelativePath( + geoAssets.geosite.providerName, + geoAssets.geosite.fileName, + ), + ); +} + @Riverpod(keepAlive: true) CoreFacade coreFacade(CoreFacadeRef ref) => CoreFacadeImpl( ref.watch(singboxServiceProvider), @@ -74,5 +115,5 @@ CoreFacade coreFacade(CoreFacadeRef ref) => CoreFacadeImpl( ref.watch(platformServicesProvider), ref.watch(clashApiProvider), ref.read(debugModeNotifierProvider), - () => ref.read(configOptionsProvider), + () => ref.read(configOptionsProvider.future), ); diff --git a/lib/data/local/dao/dao.dart b/lib/data/local/dao/dao.dart index 1d0c3ad7..e267403f 100644 --- a/lib/data/local/dao/dao.dart +++ b/lib/data/local/dao/dao.dart @@ -1 +1,2 @@ +export 'geo_assets_dao.dart'; export 'profiles_dao.dart'; diff --git a/lib/data/local/dao/geo_assets_dao.dart b/lib/data/local/dao/geo_assets_dao.dart new file mode 100644 index 00000000..3b27a9f6 --- /dev/null +++ b/lib/data/local/dao/geo_assets_dao.dart @@ -0,0 +1,36 @@ +import 'package:drift/drift.dart'; +import 'package:hiddify/data/local/data_mappers.dart'; +import 'package:hiddify/data/local/database.dart'; +import 'package:hiddify/data/local/tables.dart'; +import 'package:hiddify/domain/rules/geo_asset.dart'; +import 'package:hiddify/utils/custom_loggers.dart'; + +part 'geo_assets_dao.g.dart'; + +@DriftAccessor(tables: [GeoAssetEntries]) +class GeoAssetsDao extends DatabaseAccessor + with _$GeoAssetsDaoMixin, InfraLogger { + GeoAssetsDao(super.db); + + Future getActive(GeoAssetType type) async { + return (geoAssetEntries.select() + ..where((tbl) => tbl.active.equals(true)) + ..where((tbl) => tbl.type.equalsValue(type)) + ..limit(1)) + .map(GeoAssetMapper.fromEntry) + .getSingleOrNull(); + } + + Stream> watchAll() { + return geoAssetEntries.select().map(GeoAssetMapper.fromEntry).watch(); + } + + Future edit(GeoAsset patch) async { + await transaction( + () async { + await (update(geoAssetEntries)..where((tbl) => tbl.id.equals(patch.id))) + .write(patch.toCompanion()); + }, + ); + } +} diff --git a/lib/data/local/data_mappers.dart b/lib/data/local/data_mappers.dart index 0646a749..571afd89 100644 --- a/lib/data/local/data_mappers.dart +++ b/lib/data/local/data_mappers.dart @@ -1,6 +1,7 @@ import 'package:drift/drift.dart'; import 'package:hiddify/data/local/database.dart'; import 'package:hiddify/domain/profiles/profiles.dart'; +import 'package:hiddify/domain/rules/geo_asset.dart'; extension ProfileMapper on Profile { ProfileEntriesCompanion toCompanion() { @@ -71,3 +72,29 @@ extension ProfileMapper on Profile { }; } } + +extension GeoAssetMapper on GeoAsset { + GeoAssetEntriesCompanion toCompanion() { + return GeoAssetEntriesCompanion.insert( + id: id, + type: type, + active: active, + name: name, + providerName: providerName, + version: Value(version), + lastCheck: Value(lastCheck), + ); + } + + static GeoAsset fromEntry(GeoAssetEntry e) { + return GeoAsset( + id: e.id, + name: e.name, + type: e.type, + active: e.active, + providerName: e.providerName, + version: e.version, + lastCheck: e.lastCheck, + ); + } +} diff --git a/lib/data/local/database.dart b/lib/data/local/database.dart index a837c1ef..2800f77e 100644 --- a/lib/data/local/database.dart +++ b/lib/data/local/database.dart @@ -3,29 +3,35 @@ import 'dart:io'; import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:hiddify/data/local/dao/dao.dart'; +import 'package:hiddify/data/local/data_mappers.dart'; import 'package:hiddify/data/local/schema_versions.dart'; import 'package:hiddify/data/local/tables.dart'; import 'package:hiddify/data/local/type_converters.dart'; import 'package:hiddify/domain/profiles/profiles.dart'; +import 'package:hiddify/domain/rules/geo_asset.dart'; import 'package:hiddify/services/files_editor_service.dart'; import 'package:path/path.dart' as p; part 'database.g.dart'; -@DriftDatabase(tables: [ProfileEntries], daos: [ProfilesDao]) +@DriftDatabase( + tables: [ProfileEntries, GeoAssetEntries], + daos: [ProfilesDao, GeoAssetsDao], +) class AppDatabase extends _$AppDatabase { AppDatabase({required QueryExecutor connection}) : super(connection); AppDatabase.connect() : super(_openConnection()); @override - int get schemaVersion => 2; + int get schemaVersion => 3; @override MigrationStrategy get migration { return MigrationStrategy( onCreate: (Migrator m) async { await m.createAll(); + await _prePopulateGeoAssets(); }, onUpgrade: stepByStep( // add type column to profile entries table @@ -41,9 +47,22 @@ class AppDatabase extends _$AppDatabase { ), ); }, + from2To3: (m, schema) async { + await m.createTable(schema.geoAssetEntries); + await _prePopulateGeoAssets(); + }, ), ); } + + Future _prePopulateGeoAssets() async { + await transaction(() async { + final geoAssets = defaultGeoAssets.map((e) => e.toCompanion()); + for (final geoAsset in geoAssets) { + await into(geoAssetEntries).insert(geoAsset); + } + }); + } } LazyDatabase _openConnection() { diff --git a/lib/data/local/schema_versions.dart b/lib/data/local/schema_versions.dart index 7a743f02..da7a4d2b 100644 --- a/lib/data/local/schema_versions.dart +++ b/lib/data/local/schema_versions.dart @@ -111,8 +111,96 @@ i1.GeneratedColumn _column_11(String aliasedName) => i1.GeneratedColumn _column_12(String aliasedName) => i1.GeneratedColumn('support_url', aliasedName, true, type: i1.DriftSqlType.string); + +final class _S3 extends i0.VersionedSchema { + _S3({required super.database}) : super(version: 3); + @override + late final List entities = [ + profileEntries, + geoAssetEntries, + ]; + late final Shape0 profileEntries = Shape0( + source: i0.VersionedTable( + entityName: 'profile_entries', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_0, + _column_1, + _column_2, + _column_3, + _column_4, + _column_5, + _column_6, + _column_7, + _column_8, + _column_9, + _column_10, + _column_11, + _column_12, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape1 geoAssetEntries = Shape1( + source: i0.VersionedTable( + entityName: 'geo_asset_entries', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(id)', + 'UNIQUE(name, provider_name)', + ], + columns: [ + _column_0, + _column_1, + _column_2, + _column_3, + _column_13, + _column_14, + _column_15, + ], + attachedDatabase: database, + ), + alias: null); +} + +class Shape1 extends i0.VersionedTable { + Shape1({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get type => + columnsByName['type']! as i1.GeneratedColumn; + i1.GeneratedColumn get active => + columnsByName['active']! as i1.GeneratedColumn; + i1.GeneratedColumn get name => + columnsByName['name']! as i1.GeneratedColumn; + i1.GeneratedColumn get providerName => + columnsByName['provider_name']! as i1.GeneratedColumn; + i1.GeneratedColumn get version => + columnsByName['version']! as i1.GeneratedColumn; + i1.GeneratedColumn get lastCheck => + columnsByName['last_check']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_13(String aliasedName) => + i1.GeneratedColumn('provider_name', aliasedName, false, + additionalChecks: i1.GeneratedColumn.checkTextLength( + minTextLength: 1, + ), + type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_14(String aliasedName) => + i1.GeneratedColumn('version', aliasedName, true, + type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_15(String aliasedName) => + i1.GeneratedColumn('last_check', aliasedName, true, + type: i1.DriftSqlType.dateTime); i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, _S2 schema) from1To2, + required Future Function(i1.Migrator m, _S3 schema) from2To3, }) { return (currentVersion, database) async { switch (currentVersion) { @@ -121,6 +209,11 @@ i0.MigrationStepWithVersion migrationSteps({ final migrator = i1.Migrator(database, schema); await from1To2(migrator, schema); return 2; + case 2: + final schema = _S3(database: database); + final migrator = i1.Migrator(database, schema); + await from2To3(migrator, schema); + return 3; default: throw ArgumentError.value('Unknown migration from $currentVersion'); } @@ -129,8 +222,10 @@ i0.MigrationStepWithVersion migrationSteps({ i1.OnUpgrade stepByStep({ required Future Function(i1.Migrator m, _S2 schema) from1To2, + required Future Function(i1.Migrator m, _S3 schema) from2To3, }) => i0.VersionedSchema.stepByStepHelper( step: migrationSteps( from1To2: from1To2, + from2To3: from2To3, )); diff --git a/lib/data/local/schemas/drift_schema_v3.json b/lib/data/local/schemas/drift_schema_v3.json new file mode 100644 index 00000000..4e845f77 --- /dev/null +++ b/lib/data/local/schemas/drift_schema_v3.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.1.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"profile_entries","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(ProfileType.values)","dart_type_name":"ProfileType"}},{"name":"active","getter_name":"active","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"active\" IN (0, 1))","default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[{"allowed-lengths":{"min":1,"max":null}}]},{"name":"url","getter_name":"url","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"last_update","getter_name":"lastUpdate","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"update_interval","getter_name":"updateInterval","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"DurationTypeConverter()","dart_type_name":"Duration"}},{"name":"upload","getter_name":"upload","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"download","getter_name":"download","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"total","getter_name":"total","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"expire","getter_name":"expire","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"web_page_url","getter_name":"webPageUrl","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"support_url","getter_name":"supportUrl","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":1,"references":[],"type":"table","data":{"name":"geo_asset_entries","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(GeoAssetType.values)","dart_type_name":"GeoAssetType"}},{"name":"active","getter_name":"active","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"active\" IN (0, 1))","default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[{"allowed-lengths":{"min":1,"max":null}}]},{"name":"provider_name","getter_name":"providerName","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[{"allowed-lengths":{"min":1,"max":null}}]},{"name":"version","getter_name":"version","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"last_check","getter_name":"lastCheck","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"],"unique_keys":[["name","provider_name"]]}}]} \ No newline at end of file diff --git a/lib/data/local/tables.dart b/lib/data/local/tables.dart index 17bba834..18fe4cdc 100644 --- a/lib/data/local/tables.dart +++ b/lib/data/local/tables.dart @@ -1,6 +1,7 @@ import 'package:drift/drift.dart'; import 'package:hiddify/data/local/type_converters.dart'; import 'package:hiddify/domain/profiles/profiles.dart'; +import 'package:hiddify/domain/rules/geo_asset.dart'; @DataClassName('ProfileEntry') class ProfileEntries extends Table { @@ -22,3 +23,22 @@ class ProfileEntries extends Table { @override Set get primaryKey => {id}; } + +@DataClassName('GeoAssetEntry') +class GeoAssetEntries extends Table { + TextColumn get id => text()(); + TextColumn get type => textEnum()(); + BoolColumn get active => boolean()(); + TextColumn get name => text().withLength(min: 1)(); + TextColumn get providerName => text().withLength(min: 1)(); + TextColumn get version => text().nullable()(); + DateTimeColumn get lastCheck => dateTime().nullable()(); + + @override + Set get primaryKey => {id}; + + @override + List> get uniqueKeys => [ + {name, providerName}, + ]; +} diff --git a/lib/data/repository/config_options_store.dart b/lib/data/repository/config_options_store.dart index 55c7455c..1f181738 100644 --- a/lib/data/repository/config_options_store.dart +++ b/lib/data/repository/config_options_store.dart @@ -147,19 +147,8 @@ ConfigOptions configPreferences(ConfigPreferencesRef ref) { urlTestInterval: ref.watch(urlTestIntervalStore), enableClashApi: ref.watch(enableClashApiStore), clashApiPort: ref.watch(clashApiPortStore), - // enableTun: ref.watch(enableTunStore), - // setSystemProxy: ref.watch(setSystemProxyStore), bypassLan: ref.watch(bypassLanStore), enableFakeDns: ref.watch(enableFakeDnsStore), rules: ref.watch(rulesProvider), ); } - -@riverpod -ConfigOptions configOptions(ConfigOptionsRef ref) { - final serviceMode = ref.watch(serviceModeStoreProvider); - return ref.watch(configPreferencesProvider).copyWith( - enableTun: serviceMode == ServiceMode.tun, - setSystemProxy: serviceMode == ServiceMode.systemProxy, - ); -} diff --git a/lib/data/repository/core_facade_impl.dart b/lib/data/repository/core_facade_impl.dart index d6d88279..16220f5f 100644 --- a/lib/data/repository/core_facade_impl.dart +++ b/lib/data/repository/core_facade_impl.dart @@ -29,7 +29,7 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { final PlatformServices platformServices; final ClashApi clash; final bool debug; - final ConfigOptions Function() configOptions; + final Future Function() configOptions; bool _initialized = false; @@ -95,9 +95,9 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { String fileName, ) { return exceptionHandler( - () { + () async { final configPath = filesEditor.configPath(fileName); - final options = configOptions(); + final options = await configOptions(); return setup() .andThen(() => changeConfigOptions(options)) .andThen( @@ -119,7 +119,7 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { return exceptionHandler( () async { final configPath = filesEditor.configPath(fileName); - final options = configOptions(); + final options = await configOptions(); loggy.info( "config options: ${options.format()}\nMemory Limit: ${!disableMemoryLimit}", ); @@ -159,9 +159,9 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { bool disableMemoryLimit, ) { return exceptionHandler( - () { + () async { final configPath = filesEditor.configPath(fileName); - return changeConfigOptions(configOptions()) + return changeConfigOptions(await configOptions()) .andThen( () => singbox .restart(configPath, disableMemoryLimit) diff --git a/lib/data/repository/geo_assets_repository.dart b/lib/data/repository/geo_assets_repository.dart new file mode 100644 index 00000000..c0488aa3 --- /dev/null +++ b/lib/data/repository/geo_assets_repository.dart @@ -0,0 +1,140 @@ +import 'dart:io'; + +import 'package:dartx/dartx_io.dart'; +import 'package:dio/dio.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/data/local/dao/dao.dart'; +import 'package:hiddify/data/repository/exception_handlers.dart'; +import 'package:hiddify/domain/rules/geo_asset.dart'; +import 'package:hiddify/domain/rules/geo_asset_failure.dart'; +import 'package:hiddify/domain/rules/geo_assets_repository.dart'; +import 'package:hiddify/services/files_editor_service.dart'; +import 'package:hiddify/utils/custom_loggers.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:watcher/watcher.dart'; + +class GeoAssetsRepositoryImpl + with ExceptionHandler, InfraLogger + implements GeoAssetsRepository { + GeoAssetsRepositoryImpl({ + required this.geoAssetsDao, + required this.dio, + required this.filesEditor, + }); + + final GeoAssetsDao geoAssetsDao; + final Dio dio; + final FilesEditorService filesEditor; + + @override + TaskEither + getActivePair() { + return exceptionHandler( + () async { + final geoip = await geoAssetsDao.getActive(GeoAssetType.geoip); + final geosite = await geoAssetsDao.getActive(GeoAssetType.geosite); + if (geoip == null || geosite == null) { + return left(const GeoAssetFailure.activeAssetNotFound()); + } + return right((geoip: geoip, geosite: geosite)); + }, + GeoAssetFailure.unexpected, + ); + } + + @override + Stream>> watchAll() { + final persistedStream = geoAssetsDao.watchAll(); + final filesStream = _watchGeoFiles(); + + return Rx.combineLatest2( + persistedStream, + filesStream, + (assets, files) => assets.map( + (e) { + final path = filesEditor.geoAssetPath(e.providerName, e.fileName); + final file = files.firstOrNullWhere((e) => e.path == path); + final stat = file?.statSync(); + return (e, stat?.size); + }, + ).toList(), + ).handleExceptions(GeoAssetUnexpectedFailure.new); + } + + Iterable _geoFiles = []; + Stream> _watchGeoFiles() async* { + yield await _readGeoFiles(); + yield* Watcher( + filesEditor.geoAssetsDir.path, + pollingDelay: const Duration(seconds: 1), + ).events.asyncMap((event) async { + if (event.type == ChangeType.MODIFY) { + await _readGeoFiles(); + } + return _geoFiles; + }); + } + + Future> _readGeoFiles() async { + return _geoFiles = Directory(filesEditor.geoAssetsDir.path) + .listSync() + .whereType() + .where((e) => e.extension == '.db'); + } + + @override + TaskEither update(GeoAsset geoAsset) { + return exceptionHandler( + () async { + loggy.debug( + "checking latest release of [${geoAsset.name}] on [${geoAsset.repositoryUrl}]", + ); + final response = await dio.get(geoAsset.repositoryUrl); + if (response.statusCode != 200 || response.data == null) { + return left( + GeoAssetFailure.unexpected("invalid response", StackTrace.current), + ); + } + + final path = + filesEditor.geoAssetPath(geoAsset.providerName, geoAsset.name); + final tagName = response.data!['tag_name'] as String; + loggy.debug("latest release of [${geoAsset.name}]: [$tagName]"); + if (tagName == geoAsset.version && await File(path).exists()) { + await geoAssetsDao.edit(geoAsset.copyWith(lastCheck: DateTime.now())); + return left(const GeoAssetFailure.noUpdateAvailable()); + } + + final assets = (response.data!['assets'] as List) + .whereType>(); + final asset = + assets.firstOrNullWhere((e) => e["name"] == geoAsset.name); + if (asset == null) { + return left( + GeoAssetFailure.unexpected( + "couldn't find [${geoAsset.name}] on [${geoAsset.repositoryUrl}]", + StackTrace.current, + ), + ); + } + + final downloadUrl = asset["browser_download_url"] as String; + loggy.debug("[${geoAsset.name}] download url: [$downloadUrl]"); + final tempPath = "$path.tmp"; + await File(path).parent.create(recursive: true); + await dio.download(downloadUrl, tempPath); + await File(tempPath).rename(path); + + await geoAssetsDao.edit( + geoAsset.copyWith( + version: tagName, + lastCheck: DateTime.now(), + ), + ); + + return right(unit); + }, + GeoAssetFailure.unexpected, + ); + } +} diff --git a/lib/domain/rules/geo_asset.dart b/lib/domain/rules/geo_asset.dart new file mode 100644 index 00000000..2420d913 --- /dev/null +++ b/lib/domain/rules/geo_asset.dart @@ -0,0 +1,51 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'geo_asset.freezed.dart'; +part 'geo_asset.g.dart'; + +enum GeoAssetType { geoip, geosite } + +typedef GeoAssetWithFileSize = (GeoAsset geoAsset, int? size); + +@freezed +class GeoAsset with _$GeoAsset { + const GeoAsset._(); + + const factory GeoAsset({ + required String id, + required String name, + required GeoAssetType type, + required bool active, + required String providerName, + String? version, + DateTime? lastCheck, + }) = _GeoAsset; + + factory GeoAsset.fromJson(Map json) => + _$GeoAssetFromJson(json); + + String get fileName => name; + + String get repositoryUrl => + "https://api.github.com/repos/$providerName/releases/latest"; +} + +/// default geoip asset bundled with the app +const defaultGeoip = GeoAsset( + id: "sing-box-geoip", + name: "geoip.db", + type: GeoAssetType.geoip, + active: true, + providerName: "SagerNet/sing-geoip", +); + +/// default geosite asset bundled with the app +const defaultGeosite = GeoAsset( + id: "sing-box-geosite", + name: "geosite.db", + type: GeoAssetType.geosite, + active: true, + providerName: "SagerNet/sing-geosite", +); + +const defaultGeoAssets = [defaultGeoip, defaultGeosite]; diff --git a/lib/domain/rules/geo_asset_failure.dart b/lib/domain/rules/geo_asset_failure.dart new file mode 100644 index 00000000..7beb8ef2 --- /dev/null +++ b/lib/domain/rules/geo_asset_failure.dart @@ -0,0 +1,39 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/core/prefs/prefs.dart'; +import 'package:hiddify/domain/failures.dart'; + +part 'geo_asset_failure.freezed.dart'; + +@freezed +sealed class GeoAssetFailure with _$GeoAssetFailure, Failure { + const GeoAssetFailure._(); + + const factory GeoAssetFailure.unexpected([ + Object? error, + StackTrace? stackTrace, + ]) = GeoAssetUnexpectedFailure; + + @With() + const factory GeoAssetFailure.noUpdateAvailable() = GeoAssetNoUpdateAvailable; + + const factory GeoAssetFailure.activeAssetNotFound() = + GeoAssetActiveAssetNotFound; + + @override + ({String type, String? message}) present(TranslationsEn t) { + return switch (this) { + GeoAssetUnexpectedFailure() => ( + type: t.failure.geoAssets.unexpected, + message: null, + ), + GeoAssetNoUpdateAvailable() => ( + type: t.failure.geoAssets.notUpdate, + message: null + ), + GeoAssetActiveAssetNotFound() => ( + type: t.failure.geoAssets.activeNotFound, + message: null, + ), + }; + } +} diff --git a/lib/domain/rules/geo_assets_repository.dart b/lib/domain/rules/geo_assets_repository.dart new file mode 100644 index 00000000..1cd6f510 --- /dev/null +++ b/lib/domain/rules/geo_assets_repository.dart @@ -0,0 +1,12 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/domain/rules/geo_asset.dart'; +import 'package:hiddify/domain/rules/geo_asset_failure.dart'; + +abstract interface class GeoAssetsRepository { + TaskEither + getActivePair(); + + Stream>> watchAll(); + + TaskEither update(GeoAsset geoAsset); +} diff --git a/lib/domain/singbox/config_options.dart b/lib/domain/singbox/config_options.dart index 6301da98..78b614cc 100644 --- a/lib/domain/singbox/config_options.dart +++ b/lib/domain/singbox/config_options.dart @@ -38,6 +38,8 @@ class ConfigOptions with _$ConfigOptions { @Default(false) bool bypassLan, @Default(false) bool enableFakeDns, @Default(true) bool independentDnsCache, + @Default("geoip.db") String geoipPath, + @Default("geosite.db") String geositePath, List? rules, }) = _ConfigOptions; diff --git a/lib/features/settings/geo_assets/geo_asset_tile.dart b/lib/features/settings/geo_assets/geo_asset_tile.dart new file mode 100644 index 00000000..b05e8dc7 --- /dev/null +++ b/lib/features/settings/geo_assets/geo_asset_tile.dart @@ -0,0 +1,116 @@ +import 'package:dartx/dartx.dart'; +import 'package:flutter/material.dart'; +import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/domain/failures.dart'; +import 'package:hiddify/domain/rules/geo_asset.dart'; +import 'package:hiddify/domain/rules/geo_asset_failure.dart'; +import 'package:hiddify/features/settings/geo_assets/geo_assets_notifier.dart'; +import 'package:hiddify/utils/alerts.dart'; +import 'package:hiddify/utils/async_mutation.dart'; +import 'package:hiddify/utils/date_time_formatter.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:humanizer/humanizer.dart'; + +class GeoAssetTile extends HookConsumerWidget { + GeoAssetTile(GeoAssetWithFileSize geoAssetWithFileSize, {super.key}) + : geoAsset = geoAssetWithFileSize.$1, + size = geoAssetWithFileSize.$2; + + final GeoAsset geoAsset; + final int? size; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + final fileMissing = size == null; + + final updateMutation = useMutation( + initialOnFailure: (err) { + if (err case GeoAssetNoUpdateAvailable()) { + CustomToast(t.failure.geoAssets.notUpdate).show(context); + } else { + CustomAlertDialog.fromErr( + t.presentError(err, action: t.settings.geoAssets.failureMsg), + ).show(context); + } + }, + initialOnSuccess: () => + CustomToast.success(t.settings.geoAssets.successMsg).show(context), + ); + + return ListTile( + title: Text.rich( + TextSpan( + children: [ + TextSpan(text: geoAsset.name), + if (geoAsset.providerName.isNotBlank) + TextSpan(text: " (${geoAsset.providerName})"), + ], + ), + ), + isThreeLine: true, + subtitle: updateMutation.state.isInProgress + ? const LinearProgressIndicator() + : Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (geoAsset.version.isNotNullOrBlank) + Padding( + padding: const EdgeInsetsDirectional.only(end: 8), + child: Text( + t.settings.geoAssets.version(version: geoAsset.version!), + overflow: TextOverflow.ellipsis, + ), + ) + else + const SizedBox(), + Flexible( + child: Text.rich( + TextSpan( + children: [ + if (fileMissing) + TextSpan( + text: t.settings.geoAssets.fileMissing, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + ), + ) + else + TextSpan(text: size?.bytes().toString()), + if (geoAsset.lastCheck != null) ...[ + const TextSpan(text: " • "), + TextSpan(text: geoAsset.lastCheck!.format()), + ], + ], + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + selected: geoAsset.active, + trailing: PopupMenuButton( + itemBuilder: (context) { + return [ + PopupMenuItem( + enabled: !updateMutation.state.isInProgress, + onTap: () { + if (updateMutation.state.isInProgress) { + return; + } + updateMutation.setFuture( + ref + .read(geoAssetsNotifierProvider.notifier) + .updateGeoAsset(geoAsset), + ); + }, + child: fileMissing + ? Text(t.settings.geoAssets.download) + : Text(t.settings.geoAssets.update), + ), + ]; + }, + ), + ); + } +} diff --git a/lib/features/settings/geo_assets/geo_assets_notifier.dart b/lib/features/settings/geo_assets/geo_assets_notifier.dart new file mode 100644 index 00000000..d7669962 --- /dev/null +++ b/lib/features/settings/geo_assets/geo_assets_notifier.dart @@ -0,0 +1,28 @@ +import 'package:hiddify/data/data_providers.dart'; +import 'package:hiddify/domain/rules/geo_asset.dart'; +import 'package:hiddify/utils/custom_loggers.dart'; +import 'package:hiddify/utils/riverpod_utils.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'geo_assets_notifier.g.dart'; + +@riverpod +class GeoAssetsNotifier extends _$GeoAssetsNotifier with AppLogger { + @override + Stream> build() { + ref.disposeDelay(const Duration(seconds: 5)); + return ref + .watch(geoAssetsRepositoryProvider) + .watchAll() + .map((event) => event.getOrElse((l) => throw l)); + } + + Future updateGeoAsset(GeoAsset geoAsset) async { + await ref.read(geoAssetsRepositoryProvider).update(geoAsset).getOrElse( + (f) { + loggy.warning("error updating profile", f); + throw f; + }, + ).run(); + } +} diff --git a/lib/features/settings/geo_assets/geo_assets_page.dart b/lib/features/settings/geo_assets/geo_assets_page.dart new file mode 100644 index 00000000..b4ddfdc5 --- /dev/null +++ b/lib/features/settings/geo_assets/geo_assets_page.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/features/settings/geo_assets/geo_asset_tile.dart'; +import 'package:hiddify/features/settings/geo_assets/geo_assets_notifier.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class GeoAssetsPage extends HookConsumerWidget { + const GeoAssetsPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + final state = ref.watch(geoAssetsNotifierProvider); + + return Scaffold( + body: CustomScrollView( + slivers: [ + SliverAppBar( + title: Text(t.settings.geoAssets.pageTitle), + ), + switch (state) { + AsyncData(value: final geoAssets) => SliverList.builder( + itemBuilder: (context, index) { + final geoAsset = geoAssets[index]; + return GeoAssetTile(geoAsset); + }, + itemCount: geoAssets.length, + ), + _ => const SliverToBoxAdapter(), + }, + ], + ), + ); + } +} diff --git a/lib/features/settings/widgets/advanced_setting_tiles.dart b/lib/features/settings/widgets/advanced_setting_tiles.dart index ef916622..23f7a2d3 100644 --- a/lib/features/settings/widgets/advanced_setting_tiles.dart +++ b/lib/features/settings/widgets/advanced_setting_tiles.dart @@ -30,6 +30,13 @@ class AdvancedSettingTiles extends HookConsumerWidget { await const ConfigOptionsRoute().push(context); }, ), + ListTile( + title: Text(t.settings.geoAssets.pageTitle), + leading: const Icon(Icons.folder), + onTap: () async { + await const GeoAssetsRoute().push(context); + }, + ), if (Platform.isAndroid) ...[ ListTile( title: Text(t.settings.network.perAppProxyPageTitle), diff --git a/lib/services/files_editor_service.dart b/lib/services/files_editor_service.dart index f9a9d298..19a8bb21 100644 --- a/lib/services/files_editor_service.dart +++ b/lib/services/files_editor_service.dart @@ -1,7 +1,9 @@ import 'dart:io'; +import 'package:dartx/dartx.dart'; import 'package:flutter/services.dart'; import 'package:hiddify/domain/constants.dart'; +import 'package:hiddify/domain/rules/geo_asset.dart'; import 'package:hiddify/gen/assets.gen.dart'; import 'package:hiddify/services/platform_services.dart'; import 'package:hiddify/utils/utils.dart'; @@ -26,6 +28,9 @@ class FilesEditorService with InfraLogger { Directory(p.join(workingDir.path, Constants.configsFolderName)); Directory get logsDir => dirs.workingDir; + Directory get geoAssetsDir => + Directory(p.join(workingDir.path, "geo-assets")); + File get appLogsFile => File(p.join(logsDir.path, "app.log")); File get coreLogsFile => File(p.join(logsDir.path, "box.log")); @@ -48,6 +53,9 @@ class FilesEditorService with InfraLogger { if (!await configsDir.exists()) { await configsDir.create(recursive: true); } + if (!await geoAssetsDir.exists()) { + await geoAssetsDir.create(recursive: true); + } if (await appLogsFile.exists()) { await appLogsFile.writeAsString(""); @@ -77,6 +85,20 @@ class FilesEditorService with InfraLogger { return p.join(configsDir.path, "$fileName.json"); } + String geoAssetPath(String providerName, String fileName) { + final prefix = providerName.replaceAll("/", "-").toLowerCase(); + return p.join( + geoAssetsDir.path, + "$prefix${prefix.isBlank ? "" : "-"}$fileName", + ); + } + + /// geoasset's path relative to working directory + String geoAssetRelativePath(String providerName, String fileName) { + final fullPath = geoAssetPath(providerName, fileName); + return p.relative(fullPath, from: workingDir.path); + } + String tempConfigPath(String fileName) => configPath("temp_$fileName"); Future deleteConfig(String fileName) { @@ -85,16 +107,18 @@ class FilesEditorService with InfraLogger { Future _populateGeoAssets() async { loggy.debug('populating geo assets'); - final geoipPath = p.join(workingDir.path, Constants.geoipFileName); + final geoipPath = + geoAssetPath(defaultGeoip.providerName, defaultGeoip.fileName); if (!await File(geoipPath).exists()) { - final defaultGeoip = await rootBundle.load(Assets.core.geoip); - await File(geoipPath).writeAsBytes(defaultGeoip.buffer.asInt8List()); + final bundledGeoip = await rootBundle.load(Assets.core.geoip); + await File(geoipPath).writeAsBytes(bundledGeoip.buffer.asInt8List()); } - final geositePath = p.join(workingDir.path, Constants.geositeFileName); + final geositePath = + geoAssetPath(defaultGeosite.providerName, defaultGeosite.fileName); if (!await File(geositePath).exists()) { - final defaultGeosite = await rootBundle.load(Assets.core.geosite); - await File(geositePath).writeAsBytes(defaultGeosite.buffer.asInt8List()); + final bundledGeosite = await rootBundle.load(Assets.core.geosite); + await File(geositePath).writeAsBytes(bundledGeosite.buffer.asInt8List()); } } } diff --git a/libcore b/libcore index 89ecc6bf..2c2504f9 160000 --- a/libcore +++ b/libcore @@ -1 +1 @@ -Subproject commit 89ecc6bf1238e12bbad26182f7a06f9aaf492b9f +Subproject commit 2c2504f97145453f4fc7866982172e95b533ea73 diff --git a/test/data/local/generated_migrations/schema.dart b/test/data/local/generated_migrations/schema.dart index c8c6ff59..1c9347e9 100644 --- a/test/data/local/generated_migrations/schema.dart +++ b/test/data/local/generated_migrations/schema.dart @@ -5,6 +5,7 @@ import 'package:drift/drift.dart'; import 'package:drift/internal/migrations.dart'; import 'schema_v1.dart' as v1; import 'schema_v2.dart' as v2; +import 'schema_v3.dart' as v3; class GeneratedHelper implements SchemaInstantiationHelper { @override @@ -14,8 +15,10 @@ class GeneratedHelper implements SchemaInstantiationHelper { return v1.DatabaseAtV1(db); case 2: return v2.DatabaseAtV2(db); + case 3: + return v3.DatabaseAtV3(db); default: - throw MissingSchemaException(version, const {1, 2}); + throw MissingSchemaException(version, const {1, 2, 3}); } } } diff --git a/test/data/local/generated_migrations/schema_v3.dart b/test/data/local/generated_migrations/schema_v3.dart new file mode 100644 index 00000000..a07c41dd --- /dev/null +++ b/test/data/local/generated_migrations/schema_v3.dart @@ -0,0 +1,168 @@ +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +//@dart=2.12 +import 'package:drift/drift.dart'; + +class ProfileEntries extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + ProfileEntries(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn type = GeneratedColumn( + 'type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn active = GeneratedColumn( + 'active', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("active" IN (0, 1))')); + late final GeneratedColumn name = + GeneratedColumn('name', aliasedName, false, + additionalChecks: GeneratedColumn.checkTextLength( + minTextLength: 1, + ), + type: DriftSqlType.string, + requiredDuringInsert: true); + late final GeneratedColumn url = GeneratedColumn( + 'url', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn lastUpdate = GeneratedColumn( + 'last_update', aliasedName, false, + type: DriftSqlType.dateTime, requiredDuringInsert: true); + late final GeneratedColumn updateInterval = GeneratedColumn( + 'update_interval', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn upload = GeneratedColumn( + 'upload', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn download = GeneratedColumn( + 'download', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn total = GeneratedColumn( + 'total', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn expire = GeneratedColumn( + 'expire', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn webPageUrl = GeneratedColumn( + 'web_page_url', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn supportUrl = GeneratedColumn( + 'support_url', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + @override + List get $columns => [ + id, + type, + active, + name, + url, + lastUpdate, + updateInterval, + upload, + download, + total, + expire, + webPageUrl, + supportUrl + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'profile_entries'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + ProfileEntries createAlias(String alias) { + return ProfileEntries(attachedDatabase, alias); + } +} + +class GeoAssetEntries extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + GeoAssetEntries(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn type = GeneratedColumn( + 'type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn active = GeneratedColumn( + 'active', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("active" IN (0, 1))')); + late final GeneratedColumn name = + GeneratedColumn('name', aliasedName, false, + additionalChecks: GeneratedColumn.checkTextLength( + minTextLength: 1, + ), + type: DriftSqlType.string, + requiredDuringInsert: true); + late final GeneratedColumn providerName = + GeneratedColumn('provider_name', aliasedName, false, + additionalChecks: GeneratedColumn.checkTextLength( + minTextLength: 1, + ), + type: DriftSqlType.string, + requiredDuringInsert: true); + late final GeneratedColumn version = GeneratedColumn( + 'version', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn lastCheck = GeneratedColumn( + 'last_check', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + @override + List get $columns => + [id, type, active, name, providerName, version, lastCheck]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'geo_asset_entries'; + @override + Set get $primaryKey => {id}; + @override + List> get uniqueKeys => [ + {name, providerName}, + ]; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + GeoAssetEntries createAlias(String alias) { + return GeoAssetEntries(attachedDatabase, alias); + } +} + +class DatabaseAtV3 extends GeneratedDatabase { + DatabaseAtV3(QueryExecutor e) : super(e); + late final ProfileEntries profileEntries = ProfileEntries(this); + late final GeoAssetEntries geoAssetEntries = GeoAssetEntries(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => + [profileEntries, geoAssetEntries]; + @override + int get schemaVersion => 3; + @override + DriftDatabaseOptions get options => + const DriftDatabaseOptions(storeDateTimeAsText: true); +} diff --git a/test/data/local/migrations_test.dart b/test/data/local/migrations_test.dart index 9a9a15b4..e184117a 100644 --- a/test/data/local/migrations_test.dart +++ b/test/data/local/migrations_test.dart @@ -6,7 +6,6 @@ import 'generated_migrations/schema.dart'; void main() { late SchemaVerifier verifier; - setUpAll(() { verifier = SchemaVerifier(GeneratedHelper()); }); @@ -16,5 +15,28 @@ void main() { final db = AppDatabase(connection: connection); await verifier.migrateAndValidate(db, 2); + await db.close(); + }); + + test('upgrade from v2 to v3', () async { + final connection = await verifier.startAt(2); + final db = AppDatabase(connection: connection); + + await verifier.migrateAndValidate(db, 3); + + final prePopulated = await db.select(db.geoAssetEntries).get(); + await db.close(); + expect(prePopulated.length, equals(2)); + }); + + test('upgrade from v1 to v3 with pre-population', () async { + final connection = await verifier.startAt(1); + final db = AppDatabase(connection: connection); + + await verifier.migrateAndValidate(db, 3); + + final prePopulated = await db.select(db.geoAssetEntries).get(); + await db.close(); + expect(prePopulated.length, equals(2)); }); }