fix(sync): ignore AddOns subfolders to make sure we do not overwrite user's addons
This commit is contained in:
@@ -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,
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user