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