From e98bcf407c11b77dc09360e7509ba50644a309b9 Mon Sep 17 00:00:00 2001 From: gasaichandesu Date: Fri, 7 Aug 2026 05:15:49 +0400 Subject: [PATCH] fix(sync): ignore AddOns subfolders to make sure we do not overwrite user's addons --- lib/config.dart | 1 + .../application/client_sync_use_case.dart | 35 ++++++++++++--- .../data/game_installation_service.dart | 43 +++++++------------ 3 files changed, 46 insertions(+), 33 deletions(-) diff --git a/lib/config.dart b/lib/config.dart index 029e467..7431190 100644 --- a/lib/config.dart +++ b/lib/config.dart @@ -11,6 +11,7 @@ final class Config { static const launcherMetadataDirectoryName = '.moonwell_launcher'; static const hashCacheFileName = 'hash_cache.json'; static const ignoredVerificationDirectories = { + 'addons', 'cache', 'errors', 'logs', diff --git a/lib/features/launcher/application/client_sync_use_case.dart b/lib/features/launcher/application/client_sync_use_case.dart index da7fa52..52af861 100644 --- a/lib/features/launcher/application/client_sync_use_case.dart +++ b/lib/features/launcher/application/client_sync_use_case.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:injectable/injectable.dart'; +import 'package:moonwell_launcher/config.dart'; import 'package:moonwell_launcher/core/use_case.dart'; import 'package:moonwell_launcher/features/downloader/domain/entities/download_exceptions.dart'; import 'package:moonwell_launcher/features/downloader/domain/entities/download_progress.dart'; @@ -95,6 +96,7 @@ class ClientSyncUseCase final localSnapshot = await _installationService.scanInstallation( input.request.installationDir, isCancelled: input.isCancelled, + manifest: manifest, onProgress: (progress, currentPath, processedFiles, totalFiles) { _emit( controller, @@ -185,6 +187,7 @@ class ClientSyncUseCase final verifiedSnapshot = await _installationService.scanInstallation( input.request.installationDir, isCancelled: input.isCancelled, + manifest: manifest, onProgress: (progress, currentPath, processedFiles, totalFiles) { _emit( controller, @@ -279,7 +282,13 @@ class ClientSyncUseCase final remoteFiles = manifest.filesByPath; return snapshot.files - .where((localFile) => !remoteFiles.containsKey(localFile.path)) + .where((localFile) { + if (remoteFiles.containsKey(localFile.path)) { + return false; + } + + return !_isIgnored(localFile.path); + }) .map((file) => file.path) .toList(); } @@ -454,14 +463,21 @@ class ClientSyncUseCase final mismatchedFiles = manifest.files.where((remoteFile) { final localFile = localFilesByPath[remoteFile.path]; - return localFile == null || + final isMismatched = + localFile == null || localFile.sha256 != remoteFile.sha256 || localFile.size != remoteFile.size; + + return isMismatched; }).toList(); - final extraFiles = snapshot.files - .where((localFile) => !manifest.filesByPath.containsKey(localFile.path)) - .toList(); + final extraFiles = snapshot.files.where((localFile) { + if (manifest.filesByPath.containsKey(localFile.path)) { + return false; + } + + return !_isIgnored(localFile.path); + }).toList(); if (snapshot.buildHash != remoteBuildHash || mismatchedFiles.isNotEmpty || @@ -492,6 +508,15 @@ class ClientSyncUseCase } } + /// Файлы, которые не будут удалены во время проверки файлов. Например, пользовательские + /// аддоны, скриншоты и т.д. + bool _isIgnored(String filePath) { + final normalizedPath = filePath.replaceAll('\\', '/').toLowerCase(); + return Config.ignoredVerificationDirectories.any( + (pattern) => normalizedPath.contains(pattern.toLowerCase()), + ); + } + Future _verifyDownloadedFile({ required String installationDir, required ClientManifestFile file, diff --git a/lib/features/launcher/data/game_installation_service.dart b/lib/features/launcher/data/game_installation_service.dart index 7d5b560..e4ce881 100644 --- a/lib/features/launcher/data/game_installation_service.dart +++ b/lib/features/launcher/data/game_installation_service.dart @@ -8,10 +8,11 @@ import 'package:injectable/injectable.dart'; import 'package:moonwell_launcher/config.dart'; import 'package:moonwell_launcher/features/downloader/domain/entities/download_exceptions.dart'; import 'package:moonwell_launcher/features/downloader/domain/entities/download_progress.dart'; +import 'package:moonwell_launcher/features/launcher/domain/entities/client_hash_entry.dart'; import 'package:moonwell_launcher/features/launcher/domain/entities/client_installation_snapshot.dart'; +import 'package:moonwell_launcher/features/launcher/domain/entities/client_manifest.dart'; import 'package:moonwell_launcher/features/launcher/domain/entities/launcher_exception.dart'; import 'package:moonwell_launcher/features/launcher/domain/entities/local_client_file.dart'; -import 'package:moonwell_launcher/features/launcher/domain/entities/client_hash_entry.dart'; import 'package:path/path.dart' as p; typedef InstallationScanProgressCallback = @@ -33,6 +34,7 @@ final class GameProcessHandle { class GameInstallationService { Future scanInstallation( String installationDir, { + required ClientManifest manifest, InstallationScanProgressCallback? onProgress, FutureOr Function()? isCancelled, }) async { @@ -132,6 +134,7 @@ class GameInstallationService { 'hashCachePath': hashCachePath, 'cachedHashes': cachedHashes, 'ignoredDirectories': Config.ignoredVerificationDirectories.toList(), + 'serverFiles': manifest.files.map((f) => f.path).toList(), }, onError: errorPort.sendPort, onExit: exitPort.sendPort, @@ -343,6 +346,9 @@ Future _scanInstallationIsolateMain(Map message) async { .whereType() .map((entry) => entry.toLowerCase()) .toSet(); + final serverFiles = (message['serverFiles'] as List) + .whereType() + .toSet(); final rootDirectory = Directory(installationDir); if (!await rootDirectory.exists()) { @@ -356,6 +362,7 @@ Future _scanInstallationIsolateMain(Map message) async { rootDirectory.path, pendingFiles, ignoredDirectories, + serverFiles, ); pendingFiles.sort( @@ -424,41 +431,21 @@ Future _collectFilesInIsolate( String rootPath, List<_SerializablePendingFile> pendingFiles, Set ignoredDirectories, + Set serverFiles, ) async { - await for (final entity in directory.list(followLinks: false)) { - final relativePath = normalizeClientPath( - p.relative(entity.path, from: rootPath), - ); + for (final path in serverFiles) { + final file = File('$rootPath/$path'); - if (relativePath.isEmpty || relativePath == '.') { + if (!await file.exists()) { continue; } - final segments = p.posix.split(relativePath); - if (segments.isNotEmpty && - ignoredDirectories.contains(segments.first.toLowerCase())) { - continue; - } + final stat = await file.stat(); - if (entity is Directory) { - await _collectFilesInIsolate( - entity, - rootPath, - pendingFiles, - ignoredDirectories, - ); - continue; - } - - if (entity is! File) { - continue; - } - - final stat = await entity.stat(); pendingFiles.add( _SerializablePendingFile( - path: entity.path, - relativePath: relativePath, + path: file.path, + relativePath: normalizeClientPath(p.relative(path)), size: stat.size, modifiedMs: stat.modified.millisecondsSinceEpoch, ), -- 2.52.0