Refactor geo assets

This commit is contained in:
problematicconsumer
2023-11-25 22:00:40 +03:30
parent 6040eae6ce
commit e2d9d5e53e
29 changed files with 594 additions and 507 deletions

View File

@@ -0,0 +1,31 @@
import 'package:drift/drift.dart';
import 'package:hiddify/data/local/database.dart';
import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart';
extension GeoAssetEntityMapper on GeoAssetEntity {
GeoAssetEntriesCompanion toEntry() {
return GeoAssetEntriesCompanion.insert(
id: id,
type: type,
active: active,
name: name,
providerName: providerName,
version: Value(version),
lastCheck: Value(lastCheck),
);
}
}
extension GeoAssetEntryMapper on GeoAssetEntry {
GeoAssetEntity toEntity() {
return GeoAssetEntity(
id: id,
name: name,
type: type,
active: active,
providerName: providerName,
version: version,
lastCheck: lastCheck,
);
}
}

View File

@@ -0,0 +1,31 @@
import 'package:hiddify/data/data_providers.dart';
import 'package:hiddify/features/geo_asset/data/geo_asset_data_source.dart';
import 'package:hiddify/features/geo_asset/data/geo_asset_path_resolver.dart';
import 'package:hiddify/features/geo_asset/data/geo_asset_repository.dart';
import 'package:hiddify/services/service_providers.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'geo_asset_data_providers.g.dart';
@Riverpod(keepAlive: true)
Future<GeoAssetRepository> geoAssetRepository(GeoAssetRepositoryRef ref) async {
final repo = GeoAssetRepositoryImpl(
geoAssetDataSource: ref.watch(geoAssetDataSourceProvider),
geoAssetPathResolver: ref.watch(geoAssetPathResolverProvider),
dio: ref.watch(dioProvider),
);
await repo.init().getOrElse((l) => throw l).run();
return repo;
}
@Riverpod(keepAlive: true)
GeoAssetDataSource geoAssetDataSource(GeoAssetDataSourceRef ref) {
return GeoAssetsDao(ref.watch(appDatabaseProvider));
}
@Riverpod(keepAlive: true)
GeoAssetPathResolver geoAssetPathResolver(GeoAssetPathResolverRef ref) {
return GeoAssetPathResolver(
ref.watch(filesEditorServiceProvider).dirs.workingDir,
);
}

View File

@@ -0,0 +1,59 @@
import 'package:drift/drift.dart';
import 'package:hiddify/data/local/database.dart';
import 'package:hiddify/data/local/tables.dart';
import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart';
import 'package:hiddify/utils/custom_loggers.dart';
part 'geo_asset_data_source.g.dart';
abstract interface class GeoAssetDataSource {
Future<void> insert(GeoAssetEntriesCompanion entry);
Future<GeoAssetEntry?> getActiveAssetByType(GeoAssetType type);
Stream<List<GeoAssetEntry>> watchAll();
Future<void> patch(String id, GeoAssetEntriesCompanion entry);
}
@DriftAccessor(tables: [GeoAssetEntries])
class GeoAssetsDao extends DatabaseAccessor<AppDatabase>
with _$GeoAssetsDaoMixin, InfraLogger
implements GeoAssetDataSource {
GeoAssetsDao(super.db);
@override
Future<void> insert(GeoAssetEntriesCompanion entry) async {
await into(geoAssetEntries).insert(entry);
}
@override
Future<GeoAssetEntry?> getActiveAssetByType(GeoAssetType type) async {
return (geoAssetEntries.select()
..where((tbl) => tbl.active.equals(true))
..where((tbl) => tbl.type.equalsValue(type))
..limit(1))
.getSingleOrNull();
}
@override
Stream<List<GeoAssetEntry>> watchAll() {
return geoAssetEntries.select().watch();
}
@override
Future<void> patch(String id, GeoAssetEntriesCompanion entry) async {
await transaction(
() async {
if (entry.active.present && entry.active.value) {
final baseEntry = await (select(geoAssetEntries)
..where((tbl) => tbl.id.equals(id)))
.getSingle();
await (update(geoAssetEntries)
..where((tbl) => tbl.active.equals(true))
..where((tbl) => tbl.type.equalsValue(baseEntry.type)))
.write(const GeoAssetEntriesCompanion(active: Value(false)));
}
await (update(geoAssetEntries)..where((tbl) => tbl.id.equals(id)))
.write(entry);
},
);
}
}

View File

@@ -0,0 +1,31 @@
import 'dart:io';
import 'package:path/path.dart' as p;
class GeoAssetPathResolver {
const GeoAssetPathResolver(this._workingDir);
final Directory _workingDir;
Directory get directory => Directory(p.join(_workingDir.path, "geo-assets"));
File file(String providerName, String fileName) {
final prefix = providerName.replaceAll("/", "-").toLowerCase().trim();
return File(
p.join(
directory.path,
"$prefix${prefix.isEmpty ? "" : "-"}$fileName",
),
);
}
/// geoasset's path relative to working directory
String relativePath(String providerName, String fileName) {
final fullPath = file(providerName, fileName).path;
return p.relative(fullPath, from: _workingDir.path);
}
String resolvePath(String path) {
return p.absolute(_workingDir.path, path);
}
}

View File

@@ -0,0 +1,232 @@
import 'dart:io';
import 'package:dartx/dartx_io.dart';
import 'package:dio/dio.dart';
import 'package:drift/drift.dart';
import 'package:flutter/services.dart';
import 'package:fpdart/fpdart.dart';
import 'package:hiddify/data/local/database.dart';
import 'package:hiddify/data/repository/exception_handlers.dart';
import 'package:hiddify/features/geo_asset/data/geo_asset_data_mapper.dart';
import 'package:hiddify/features/geo_asset/data/geo_asset_data_source.dart';
import 'package:hiddify/features/geo_asset/data/geo_asset_path_resolver.dart';
import 'package:hiddify/features/geo_asset/model/default_geo_assets.dart';
import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart';
import 'package:hiddify/features/geo_asset/model/geo_asset_failure.dart';
import 'package:hiddify/gen/assets.gen.dart';
import 'package:hiddify/utils/custom_loggers.dart';
import 'package:rxdart/rxdart.dart';
import 'package:watcher/watcher.dart';
abstract interface class GeoAssetRepository {
/// populate bundled geo assets directory with bundled files if needed
TaskEither<GeoAssetFailure, Unit> init();
TaskEither<GeoAssetFailure, ({GeoAssetEntity geoip, GeoAssetEntity geosite})>
getActivePair();
Stream<Either<GeoAssetFailure, List<GeoAssetWithFileSize>>> watchAll();
TaskEither<GeoAssetFailure, Unit> update(GeoAssetEntity geoAsset);
TaskEither<GeoAssetFailure, Unit> markAsActive(GeoAssetEntity geoAsset);
TaskEither<GeoAssetFailure, Unit> addRecommended();
}
class GeoAssetRepositoryImpl
with ExceptionHandler, InfraLogger
implements GeoAssetRepository {
GeoAssetRepositoryImpl({
required this.geoAssetDataSource,
required this.geoAssetPathResolver,
required this.dio,
});
final GeoAssetDataSource geoAssetDataSource;
final GeoAssetPathResolver geoAssetPathResolver;
final Dio dio;
@override
TaskEither<GeoAssetFailure, Unit> init() {
return exceptionHandler(
() async {
loggy.debug("initializing");
final geoipFile = geoAssetPathResolver.file(
defaultGeoip.providerName,
defaultGeoip.fileName,
);
final geositeFile = geoAssetPathResolver.file(
defaultGeosite.providerName,
defaultGeosite.fileName,
);
final dirExists = await geoAssetPathResolver.directory.exists();
if (!dirExists) {
await geoAssetPathResolver.directory.create(recursive: true);
}
if (!dirExists || !await geoipFile.exists()) {
final bundledGeoip = await rootBundle.load(Assets.core.geoip);
await geoipFile.writeAsBytes(bundledGeoip.buffer.asInt8List());
}
if (!dirExists || !await geositeFile.exists()) {
final bundledGeosite = await rootBundle.load(Assets.core.geosite);
await geositeFile.writeAsBytes(bundledGeosite.buffer.asInt8List());
}
return right(unit);
},
GeoAssetUnexpectedFailure.new,
);
}
@override
TaskEither<GeoAssetFailure, ({GeoAssetEntity geoip, GeoAssetEntity geosite})>
getActivePair() {
return exceptionHandler(
() async {
final geoip =
await geoAssetDataSource.getActiveAssetByType(GeoAssetType.geoip);
final geosite =
await geoAssetDataSource.getActiveAssetByType(GeoAssetType.geosite);
if (geoip == null || geosite == null) {
return left(const GeoAssetFailure.activeAssetNotFound());
}
return right((geoip: geoip.toEntity(), geosite: geosite.toEntity()));
},
GeoAssetUnexpectedFailure.new,
);
}
@override
Stream<Either<GeoAssetFailure, List<GeoAssetWithFileSize>>> watchAll() {
final persistedStream = geoAssetDataSource
.watchAll()
.map((event) => event.map((e) => e.toEntity()));
final filesStream = _watchGeoFiles();
return Rx.combineLatest2(
persistedStream,
filesStream,
(assets, files) => assets.map(
(e) {
final path =
geoAssetPathResolver.file(e.providerName, e.fileName).path;
final file = files.firstOrNullWhere((e) => e.path == path);
final stat = file?.statSync();
return (e, stat?.size);
},
).toList(),
).handleExceptions(GeoAssetUnexpectedFailure.new);
}
Iterable<File> _geoFiles = [];
Stream<Iterable<File>> _watchGeoFiles() async* {
yield await _readGeoFiles();
yield* Watcher(
geoAssetPathResolver.directory.path,
pollingDelay: const Duration(seconds: 1),
).events.asyncMap((event) async {
await _readGeoFiles();
return _geoFiles;
});
}
Future<Iterable<File>> _readGeoFiles() async {
return _geoFiles = Directory(geoAssetPathResolver.directory.path)
.listSync()
.whereType<File>()
.where((e) => e.extension == '.db');
}
@override
TaskEither<GeoAssetFailure, Unit> update(GeoAssetEntity geoAsset) {
return exceptionHandler(
() async {
loggy.debug(
"checking latest release of [${geoAsset.name}] on [${geoAsset.repositoryUrl}]",
);
final response = await dio.get<Map>(geoAsset.repositoryUrl);
if (response.statusCode != 200 || response.data == null) {
return left(
GeoAssetUnexpectedFailure.new(
"invalid response",
StackTrace.current,
),
);
}
final file =
geoAssetPathResolver.file(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.exists()) {
await geoAssetDataSource.patch(
geoAsset.id,
GeoAssetEntriesCompanion(lastCheck: Value(DateTime.now())),
);
return left(const GeoAssetFailure.noUpdateAvailable());
}
final assets = (response.data!['assets'] as List)
.whereType<Map<String, dynamic>>();
final asset =
assets.firstOrNullWhere((e) => e["name"] == geoAsset.name);
if (asset == null) {
return left(
GeoAssetUnexpectedFailure.new(
"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 = "${file.path}.tmp";
await file.parent.create(recursive: true);
await dio.download(downloadUrl, tempPath);
await File(tempPath).rename(file.path);
await geoAssetDataSource.patch(
geoAsset.id,
GeoAssetEntriesCompanion(
version: Value(tagName),
lastCheck: Value(DateTime.now()),
),
);
return right(unit);
},
GeoAssetUnexpectedFailure.new,
);
}
@override
TaskEither<GeoAssetFailure, Unit> markAsActive(GeoAssetEntity geoAsset) {
return exceptionHandler(
() async {
await geoAssetDataSource.patch(
geoAsset.id,
const GeoAssetEntriesCompanion(active: Value(true)),
);
return right(unit);
},
GeoAssetUnexpectedFailure.new,
);
}
@override
TaskEither<GeoAssetFailure, Unit> addRecommended() {
return exceptionHandler(
() async {
final persistedIds = await geoAssetDataSource
.watchAll()
.first
.then((value) => value.map((e) => e.id));
final missing =
recommendedGeoAssets.where((e) => !persistedIds.contains(e.id));
for (final geoAsset in missing) {
await geoAssetDataSource.insert(geoAsset.toEntry());
}
return right(unit);
},
GeoAssetUnexpectedFailure.new,
);
}
}