базовая логика скачивания обновлений

This commit is contained in:
2026-03-22 01:04:16 +04:00
parent 4b1cecbefd
commit 73d8798bb9
39 changed files with 2372 additions and 1017 deletions
@@ -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,
});
}