fix(sync): ignore AddOns subfolders to make sure we do not overwrite user's addons #2

Merged
sindoring merged 1 commits from hotfix/ignore-addons into master 2026-08-07 10:37:24 +03:00
3 changed files with 46 additions and 33 deletions
+1
View File
@@ -11,6 +11,7 @@ final class Config {
static const launcherMetadataDirectoryName = '.moonwell_launcher'; static const launcherMetadataDirectoryName = '.moonwell_launcher';
static const hashCacheFileName = 'hash_cache.json'; static const hashCacheFileName = 'hash_cache.json';
static const ignoredVerificationDirectories = <String>{ static const ignoredVerificationDirectories = <String>{
'addons',
'cache', 'cache',
'errors', 'errors',
'logs', 'logs',
@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
import 'package:moonwell_launcher/config.dart';
import 'package:moonwell_launcher/core/use_case.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_exceptions.dart';
import 'package:moonwell_launcher/features/downloader/domain/entities/download_progress.dart'; import 'package:moonwell_launcher/features/downloader/domain/entities/download_progress.dart';
@@ -95,6 +96,7 @@ class ClientSyncUseCase
final localSnapshot = await _installationService.scanInstallation( final localSnapshot = await _installationService.scanInstallation(
input.request.installationDir, input.request.installationDir,
isCancelled: input.isCancelled, isCancelled: input.isCancelled,
manifest: manifest,
onProgress: (progress, currentPath, processedFiles, totalFiles) { onProgress: (progress, currentPath, processedFiles, totalFiles) {
_emit( _emit(
controller, controller,
@@ -185,6 +187,7 @@ class ClientSyncUseCase
final verifiedSnapshot = await _installationService.scanInstallation( final verifiedSnapshot = await _installationService.scanInstallation(
input.request.installationDir, input.request.installationDir,
isCancelled: input.isCancelled, isCancelled: input.isCancelled,
manifest: manifest,
onProgress: (progress, currentPath, processedFiles, totalFiles) { onProgress: (progress, currentPath, processedFiles, totalFiles) {
_emit( _emit(
controller, controller,
@@ -279,7 +282,13 @@ class ClientSyncUseCase
final remoteFiles = manifest.filesByPath; final remoteFiles = manifest.filesByPath;
return snapshot.files 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) .map((file) => file.path)
.toList(); .toList();
} }
@@ -454,14 +463,21 @@ class ClientSyncUseCase
final mismatchedFiles = manifest.files.where((remoteFile) { final mismatchedFiles = manifest.files.where((remoteFile) {
final localFile = localFilesByPath[remoteFile.path]; final localFile = localFilesByPath[remoteFile.path];
return localFile == null || final isMismatched =
localFile == null ||
localFile.sha256 != remoteFile.sha256 || localFile.sha256 != remoteFile.sha256 ||
localFile.size != remoteFile.size; localFile.size != remoteFile.size;
return isMismatched;
}).toList(); }).toList();
final extraFiles = snapshot.files final extraFiles = snapshot.files.where((localFile) {
.where((localFile) => !manifest.filesByPath.containsKey(localFile.path)) if (manifest.filesByPath.containsKey(localFile.path)) {
.toList(); return false;
}
return !_isIgnored(localFile.path);
}).toList();
if (snapshot.buildHash != remoteBuildHash || if (snapshot.buildHash != remoteBuildHash ||
mismatchedFiles.isNotEmpty || 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<void> _verifyDownloadedFile({ Future<void> _verifyDownloadedFile({
required String installationDir, required String installationDir,
required ClientManifestFile file, required ClientManifestFile file,
@@ -8,10 +8,11 @@ import 'package:injectable/injectable.dart';
import 'package:moonwell_launcher/config.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_exceptions.dart';
import 'package:moonwell_launcher/features/downloader/domain/entities/download_progress.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_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/launcher_exception.dart';
import 'package:moonwell_launcher/features/launcher/domain/entities/local_client_file.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; import 'package:path/path.dart' as p;
typedef InstallationScanProgressCallback = typedef InstallationScanProgressCallback =
@@ -33,6 +34,7 @@ final class GameProcessHandle {
class GameInstallationService { class GameInstallationService {
Future<ClientInstallationSnapshot> scanInstallation( Future<ClientInstallationSnapshot> scanInstallation(
String installationDir, { String installationDir, {
required ClientManifest manifest,
InstallationScanProgressCallback? onProgress, InstallationScanProgressCallback? onProgress,
FutureOr<bool> Function()? isCancelled, FutureOr<bool> Function()? isCancelled,
}) async { }) async {
@@ -132,6 +134,7 @@ class GameInstallationService {
'hashCachePath': hashCachePath, 'hashCachePath': hashCachePath,
'cachedHashes': cachedHashes, 'cachedHashes': cachedHashes,
'ignoredDirectories': Config.ignoredVerificationDirectories.toList(), 'ignoredDirectories': Config.ignoredVerificationDirectories.toList(),
'serverFiles': manifest.files.map((f) => f.path).toList(),
}, },
onError: errorPort.sendPort, onError: errorPort.sendPort,
onExit: exitPort.sendPort, onExit: exitPort.sendPort,
@@ -343,6 +346,9 @@ Future<void> _scanInstallationIsolateMain(Map<String, Object?> message) async {
.whereType<String>() .whereType<String>()
.map((entry) => entry.toLowerCase()) .map((entry) => entry.toLowerCase())
.toSet(); .toSet();
final serverFiles = (message['serverFiles'] as List<Object?>)
.whereType<String>()
.toSet();
final rootDirectory = Directory(installationDir); final rootDirectory = Directory(installationDir);
if (!await rootDirectory.exists()) { if (!await rootDirectory.exists()) {
@@ -356,6 +362,7 @@ Future<void> _scanInstallationIsolateMain(Map<String, Object?> message) async {
rootDirectory.path, rootDirectory.path,
pendingFiles, pendingFiles,
ignoredDirectories, ignoredDirectories,
serverFiles,
); );
pendingFiles.sort( pendingFiles.sort(
@@ -424,41 +431,21 @@ Future<void> _collectFilesInIsolate(
String rootPath, String rootPath,
List<_SerializablePendingFile> pendingFiles, List<_SerializablePendingFile> pendingFiles,
Set<String> ignoredDirectories, Set<String> ignoredDirectories,
Set<String> serverFiles,
) async { ) async {
await for (final entity in directory.list(followLinks: false)) { for (final path in serverFiles) {
final relativePath = normalizeClientPath( final file = File('$rootPath/$path');
p.relative(entity.path, from: rootPath),
);
if (relativePath.isEmpty || relativePath == '.') { if (!await file.exists()) {
continue; continue;
} }
final segments = p.posix.split(relativePath); final stat = await file.stat();
if (segments.isNotEmpty &&
ignoredDirectories.contains(segments.first.toLowerCase())) {
continue;
}
if (entity is Directory) {
await _collectFilesInIsolate(
entity,
rootPath,
pendingFiles,
ignoredDirectories,
);
continue;
}
if (entity is! File) {
continue;
}
final stat = await entity.stat();
pendingFiles.add( pendingFiles.add(
_SerializablePendingFile( _SerializablePendingFile(
path: entity.path, path: file.path,
relativePath: relativePath, relativePath: normalizeClientPath(p.relative(path)),
size: stat.size, size: stat.size,
modifiedMs: stat.modified.millisecondsSinceEpoch, modifiedMs: stat.modified.millisecondsSinceEpoch,
), ),