базовая логика скачивания обновлений
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
abstract interface class ClientHashEntry {
|
||||
String get path;
|
||||
int get size;
|
||||
String get sha256;
|
||||
}
|
||||
|
||||
String normalizeClientPath(String rawPath) {
|
||||
final sanitized = rawPath.replaceAll('\\', '/').trim();
|
||||
final normalized = p.posix.normalize(sanitized);
|
||||
return normalized.startsWith('./') ? normalized.substring(2) : normalized;
|
||||
}
|
||||
|
||||
String computeBuildHash(Iterable<ClientHashEntry> entries) {
|
||||
final canonicalEntries =
|
||||
entries
|
||||
.map(
|
||||
(entry) =>
|
||||
'${normalizeClientPath(entry.path)}:${entry.size}:${entry.sha256.toLowerCase()}',
|
||||
)
|
||||
.toList()
|
||||
..sort();
|
||||
|
||||
final payload = canonicalEntries.join('\n');
|
||||
return sha256.convert(utf8.encode(payload)).toString();
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'client_hash_entry.dart';
|
||||
import 'local_client_file.dart';
|
||||
|
||||
final class ClientInstallationSnapshot {
|
||||
final List<LocalClientFile> files;
|
||||
final String buildHash;
|
||||
|
||||
const ClientInstallationSnapshot({
|
||||
required this.files,
|
||||
required this.buildHash,
|
||||
});
|
||||
|
||||
factory ClientInstallationSnapshot.fromFiles(List<LocalClientFile> files) {
|
||||
return ClientInstallationSnapshot(
|
||||
files: files,
|
||||
buildHash: computeBuildHash(files),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, LocalClientFile> get filesByPath => {
|
||||
for (final file in files) normalizeClientPath(file.path): file,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import 'client_hash_entry.dart';
|
||||
import 'client_manifest_file.dart';
|
||||
|
||||
final class ClientManifest {
|
||||
final List<ClientManifestFile> files;
|
||||
final String buildHash;
|
||||
|
||||
const ClientManifest({required this.files, required this.buildHash});
|
||||
|
||||
factory ClientManifest.fromJson(Map<String, dynamic> json) {
|
||||
final files =
|
||||
((json['files'] as List<dynamic>? ?? const <dynamic>[])
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(ClientManifestFile.fromJson)
|
||||
.toList())
|
||||
.cast<ClientManifestFile>();
|
||||
|
||||
return ClientManifest(files: files, buildHash: computeBuildHash(files));
|
||||
}
|
||||
|
||||
Map<String, ClientManifestFile> get filesByPath => {
|
||||
for (final file in files) normalizeClientPath(file.path): file,
|
||||
};
|
||||
|
||||
int get totalSize => files.fold(0, (sum, file) => sum + file.size);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import 'client_hash_entry.dart';
|
||||
|
||||
final class ClientManifestFile implements ClientHashEntry {
|
||||
@override
|
||||
final String path;
|
||||
|
||||
@override
|
||||
final int size;
|
||||
|
||||
@override
|
||||
final String sha256;
|
||||
|
||||
const ClientManifestFile({
|
||||
required this.path,
|
||||
required this.size,
|
||||
required this.sha256,
|
||||
});
|
||||
|
||||
factory ClientManifestFile.fromJson(Map<String, dynamic> json) {
|
||||
return ClientManifestFile(
|
||||
path: normalizeClientPath(json['path'] as String? ?? ''),
|
||||
size: (json['size'] as num? ?? 0).toInt(),
|
||||
sha256: (json['sha256'] as String? ?? '').toLowerCase(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import 'package:moonwell_launcher/features/downloader/domain/entities/download_progress.dart';
|
||||
|
||||
enum ClientSyncStage {
|
||||
fetchingManifest,
|
||||
scanningLocalFiles,
|
||||
comparingFiles,
|
||||
removingStaleFiles,
|
||||
downloadingFiles,
|
||||
verifyingInstallation,
|
||||
completed,
|
||||
}
|
||||
|
||||
final class ClientSyncStatus {
|
||||
final ClientSyncStage stage;
|
||||
final DownloadProgress progress;
|
||||
final String message;
|
||||
final String? currentPath;
|
||||
final String? localBuildHash;
|
||||
final String? remoteBuildHash;
|
||||
final int processedFiles;
|
||||
final int totalFiles;
|
||||
|
||||
const ClientSyncStatus({
|
||||
required this.stage,
|
||||
required this.progress,
|
||||
required this.message,
|
||||
required this.currentPath,
|
||||
required this.localBuildHash,
|
||||
required this.remoteBuildHash,
|
||||
required this.processedFiles,
|
||||
required this.totalFiles,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
sealed class LauncherException implements Exception {
|
||||
final String message;
|
||||
|
||||
const LauncherException(this.message);
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
|
||||
final class LauncherConfigurationException extends LauncherException {
|
||||
const LauncherConfigurationException(super.message);
|
||||
}
|
||||
|
||||
final class LauncherApiException extends LauncherException {
|
||||
const LauncherApiException(super.message);
|
||||
}
|
||||
|
||||
final class LauncherSyncException extends LauncherException {
|
||||
const LauncherSyncException(super.message);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
final class LauncherSession {
|
||||
final String accessToken;
|
||||
final String tokenType;
|
||||
final DateTime? expiresAt;
|
||||
|
||||
const LauncherSession({
|
||||
required this.accessToken,
|
||||
required this.tokenType,
|
||||
required this.expiresAt,
|
||||
});
|
||||
|
||||
factory LauncherSession.fromJson(Map<String, dynamic> json) {
|
||||
return LauncherSession(
|
||||
accessToken: json['access_token'] as String? ?? '',
|
||||
tokenType: json['token_type'] as String? ?? 'Bearer',
|
||||
expiresAt: _parseDateTime(json['expires_at'] as String?),
|
||||
);
|
||||
}
|
||||
|
||||
static DateTime? _parseDateTime(String? rawValue) {
|
||||
if (rawValue == null || rawValue.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return DateTime.tryParse(rawValue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'client_hash_entry.dart';
|
||||
|
||||
final class LocalClientFile implements ClientHashEntry {
|
||||
@override
|
||||
final String path;
|
||||
|
||||
@override
|
||||
final int size;
|
||||
|
||||
@override
|
||||
final String sha256;
|
||||
|
||||
const LocalClientFile({
|
||||
required this.path,
|
||||
required this.size,
|
||||
required this.sha256,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user