187 lines
5.6 KiB
Dart
187 lines
5.6 KiB
Dart
|
|
import 'dart:convert';
|
||
|
|
|
||
|
|
import '../../contracts/contracts.dart';
|
||
|
|
import '../errors.dart';
|
||
|
|
import '../ports/danmaku_sources_store.dart';
|
||
|
|
import '../ports/script_widget_repository.dart';
|
||
|
|
import '../ports/server_repository.dart';
|
||
|
|
import '../ports/session_repository.dart';
|
||
|
|
|
||
|
|
const int kBackupSchemaVersion = 1;
|
||
|
|
|
||
|
|
class BackupExportResult {
|
||
|
|
final String json;
|
||
|
|
final int serverCount;
|
||
|
|
final int sessionCount;
|
||
|
|
final int danmakuSourceCount;
|
||
|
|
final int scriptWidgetCount;
|
||
|
|
final int scriptWidgetSubscriptionCount;
|
||
|
|
|
||
|
|
const BackupExportResult({
|
||
|
|
required this.json,
|
||
|
|
required this.serverCount,
|
||
|
|
required this.sessionCount,
|
||
|
|
required this.danmakuSourceCount,
|
||
|
|
required this.scriptWidgetCount,
|
||
|
|
required this.scriptWidgetSubscriptionCount,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
class BackupImportResult {
|
||
|
|
final int serverCount;
|
||
|
|
final int sessionCount;
|
||
|
|
final int danmakuSourceCount;
|
||
|
|
final int scriptWidgetCount;
|
||
|
|
final int scriptWidgetSubscriptionCount;
|
||
|
|
|
||
|
|
const BackupImportResult({
|
||
|
|
required this.serverCount,
|
||
|
|
required this.sessionCount,
|
||
|
|
required this.danmakuSourceCount,
|
||
|
|
required this.scriptWidgetCount,
|
||
|
|
required this.scriptWidgetSubscriptionCount,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
class ExportBackupUseCase {
|
||
|
|
final ServerRepository serverRepository;
|
||
|
|
final SessionRepository sessionRepository;
|
||
|
|
final DanmakuSourcesStore danmakuSourcesStore;
|
||
|
|
final ScriptWidgetRepository scriptWidgetRepository;
|
||
|
|
final DateTime Function() now;
|
||
|
|
|
||
|
|
ExportBackupUseCase({
|
||
|
|
required this.serverRepository,
|
||
|
|
required this.sessionRepository,
|
||
|
|
required this.danmakuSourcesStore,
|
||
|
|
required this.scriptWidgetRepository,
|
||
|
|
DateTime Function()? now,
|
||
|
|
}) : now = (now ?? DateTime.now);
|
||
|
|
|
||
|
|
Future<BackupExportResult> execute() async {
|
||
|
|
final servers = await serverRepository.list();
|
||
|
|
final sessions = await sessionRepository.listAll();
|
||
|
|
final danmakuSources = await danmakuSourcesStore.list();
|
||
|
|
final scriptWidgets = await scriptWidgetRepository.listWidgets();
|
||
|
|
final scriptWidgetSubscriptions =
|
||
|
|
await scriptWidgetRepository.listSubscriptions();
|
||
|
|
final bundle = BackupBundle(
|
||
|
|
version: kBackupSchemaVersion,
|
||
|
|
exportedAt: now().toUtc().toIso8601String(),
|
||
|
|
servers: servers,
|
||
|
|
sessions: sessions,
|
||
|
|
danmakuSources: danmakuSources,
|
||
|
|
scriptWidgets: scriptWidgets,
|
||
|
|
scriptWidgetSubscriptions: scriptWidgetSubscriptions,
|
||
|
|
);
|
||
|
|
final json = const JsonEncoder.withIndent(' ').convert(bundle.toJson());
|
||
|
|
return BackupExportResult(
|
||
|
|
json: json,
|
||
|
|
serverCount: servers.length,
|
||
|
|
sessionCount: sessions.length,
|
||
|
|
danmakuSourceCount: danmakuSources.length,
|
||
|
|
scriptWidgetCount: scriptWidgets.length,
|
||
|
|
scriptWidgetSubscriptionCount: scriptWidgetSubscriptions.length,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class ImportBackupUseCase {
|
||
|
|
final ServerRepository serverRepository;
|
||
|
|
final SessionRepository sessionRepository;
|
||
|
|
final DanmakuSourcesStore danmakuSourcesStore;
|
||
|
|
final ScriptWidgetRepository scriptWidgetRepository;
|
||
|
|
|
||
|
|
ImportBackupUseCase({
|
||
|
|
required this.serverRepository,
|
||
|
|
required this.sessionRepository,
|
||
|
|
required this.danmakuSourcesStore,
|
||
|
|
required this.scriptWidgetRepository,
|
||
|
|
});
|
||
|
|
|
||
|
|
Future<BackupImportResult> execute(String rawJson) async {
|
||
|
|
final BackupBundle bundle;
|
||
|
|
try {
|
||
|
|
final decoded = jsonDecode(rawJson);
|
||
|
|
if (decoded is! Map<String, dynamic>) {
|
||
|
|
throw DomainError(ErrorCode.invalidParams, '备份文件格式不正确');
|
||
|
|
}
|
||
|
|
bundle = BackupBundle.fromJson(decoded);
|
||
|
|
} on DomainError {
|
||
|
|
rethrow;
|
||
|
|
} catch (e) {
|
||
|
|
throw DomainError(
|
||
|
|
ErrorCode.invalidParams,
|
||
|
|
'备份文件解析失败',
|
||
|
|
details: {'reason': e.toString()},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (bundle.version != kBackupSchemaVersion) {
|
||
|
|
throw DomainError(
|
||
|
|
ErrorCode.invalidParams,
|
||
|
|
'不支持的备份文件版本:${bundle.version}',
|
||
|
|
details: {'expected': kBackupSchemaVersion, 'got': bundle.version},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
for (final server in bundle.servers) {
|
||
|
|
await serverRepository.update(server);
|
||
|
|
}
|
||
|
|
await sessionRepository.setMany(bundle.sessions);
|
||
|
|
|
||
|
|
final mergedSources = mergeDanmakuSourcesByUrl(
|
||
|
|
local: await danmakuSourcesStore.list(),
|
||
|
|
remote: bundle.danmakuSources,
|
||
|
|
);
|
||
|
|
await danmakuSourcesStore.replaceAll(mergedSources);
|
||
|
|
|
||
|
|
for (final scriptWidget in bundle.scriptWidgets) {
|
||
|
|
await scriptWidgetRepository.upsertWidget(scriptWidget);
|
||
|
|
}
|
||
|
|
for (final subscription in bundle.scriptWidgetSubscriptions) {
|
||
|
|
await scriptWidgetRepository.upsertSubscription(subscription);
|
||
|
|
}
|
||
|
|
|
||
|
|
return BackupImportResult(
|
||
|
|
serverCount: bundle.servers.length,
|
||
|
|
sessionCount: bundle.sessions.length,
|
||
|
|
danmakuSourceCount: mergedSources.length,
|
||
|
|
scriptWidgetCount: bundle.scriptWidgets.length,
|
||
|
|
scriptWidgetSubscriptionCount: bundle.scriptWidgetSubscriptions.length,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
List<DanmakuSource> mergeDanmakuSourcesByUrl({
|
||
|
|
required List<DanmakuSource> local,
|
||
|
|
required List<DanmakuSource> remote,
|
||
|
|
}) {
|
||
|
|
final localByUrl = <String, DanmakuSource>{
|
||
|
|
for (final s in local) s.url.trim(): s,
|
||
|
|
};
|
||
|
|
final out = <DanmakuSource>[];
|
||
|
|
final seen = <String>{};
|
||
|
|
for (final r in remote) {
|
||
|
|
final url = r.url.trim();
|
||
|
|
if (url.isEmpty || seen.contains(url)) continue;
|
||
|
|
seen.add(url);
|
||
|
|
final hit = localByUrl[url];
|
||
|
|
if (hit != null) {
|
||
|
|
out.add(hit.copyWith(name: hit.name.trim().isEmpty ? r.name : hit.name));
|
||
|
|
} else {
|
||
|
|
out.add(
|
||
|
|
DanmakuSource(
|
||
|
|
id: r.id.trim().isEmpty
|
||
|
|
? 'src_${url.hashCode.toUnsigned(32).toRadixString(16)}'
|
||
|
|
: r.id,
|
||
|
|
name: r.name,
|
||
|
|
url: url,
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return out;
|
||
|
|
}
|