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

This commit is contained in:
gasaichandesu
2026-08-07 05:15:49 +04:00
parent f395d2515d
commit e98bcf407c
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 hashCacheFileName = 'hash_cache.json';
static const ignoredVerificationDirectories = <String>{
'addons',
'cache',
'errors',
'logs',
@@ -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<void> _verifyDownloadedFile({
required String installationDir,
required ClientManifestFile file,
@@ -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<ClientInstallationSnapshot> scanInstallation(
String installationDir, {
required ClientManifest manifest,
InstallationScanProgressCallback? onProgress,
FutureOr<bool> 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<void> _scanInstallationIsolateMain(Map<String, Object?> message) async {
.whereType<String>()
.map((entry) => entry.toLowerCase())
.toSet();
final serverFiles = (message['serverFiles'] as List<Object?>)
.whereType<String>()
.toSet();
final rootDirectory = Directory(installationDir);
if (!await rootDirectory.exists()) {
@@ -356,6 +362,7 @@ Future<void> _scanInstallationIsolateMain(Map<String, Object?> message) async {
rootDirectory.path,
pendingFiles,
ignoredDirectories,
serverFiles,
);
pendingFiles.sort(
@@ -424,41 +431,21 @@ Future<void> _collectFilesInIsolate(
String rootPath,
List<_SerializablePendingFile> pendingFiles,
Set<String> ignoredDirectories,
Set<String> 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,
),