обновленная синхронизация клиента
This commit is contained in:
@@ -43,6 +43,10 @@ class GameInstallationService {
|
||||
InstallationScanProgressCallback? onProgress,
|
||||
FutureOr<bool> Function()? isCancelled,
|
||||
}) async {
|
||||
// Validate the entire manifest before scanning or removing any files.
|
||||
for (final file in manifest.files) {
|
||||
resolveClientPath(installationDir, file.path);
|
||||
}
|
||||
final rootDirectory = Directory(installationDir);
|
||||
if (!await rootDirectory.exists()) {
|
||||
return ClientInstallationSnapshot.fromFiles(const []);
|
||||
@@ -103,7 +107,12 @@ class GameInstallationService {
|
||||
}
|
||||
|
||||
if (type == 'done') {
|
||||
final snapshot = ClientInstallationSnapshot.fromFiles(files);
|
||||
final snapshot = ClientInstallationSnapshot.fromFiles(
|
||||
files,
|
||||
stalePaths: (message['stalePaths'] as List<Object?>)
|
||||
.whereType<String>()
|
||||
.toList(),
|
||||
);
|
||||
completer.complete(snapshot);
|
||||
await cleanup(killIsolate: false);
|
||||
}
|
||||
@@ -138,7 +147,6 @@ class GameInstallationService {
|
||||
'installationDir': installationDir,
|
||||
'hashCachePath': hashCachePath,
|
||||
'cachedHashes': cachedHashes,
|
||||
'ignoredDirectories': Config.ignoredVerificationDirectories.toList(),
|
||||
'serverFiles': manifest.files.map((f) => f.path).toList(),
|
||||
},
|
||||
onError: errorPort.sendPort,
|
||||
@@ -182,9 +190,20 @@ class GameInstallationService {
|
||||
String installationDir,
|
||||
String relativePath,
|
||||
) async {
|
||||
final file = File(resolveClientPath(installationDir, relativePath));
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
final filePath = resolveClientPath(installationDir, relativePath);
|
||||
final type = await FileSystemEntity.type(filePath, followLinks: false);
|
||||
if (type == FileSystemEntityType.notFound) {
|
||||
return;
|
||||
}
|
||||
await _ensureParentInsideInstallation(installationDir, filePath);
|
||||
if (type == FileSystemEntityType.link) {
|
||||
await Link(filePath).delete();
|
||||
} else if (type == FileSystemEntityType.directory) {
|
||||
// Stale directories are collected after their children. Never recursively
|
||||
// delete a directory: files created since the scan must not be lost.
|
||||
await Directory(filePath).delete();
|
||||
} else if (type == FileSystemEntityType.file) {
|
||||
await File(filePath).delete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,7 +413,9 @@ class GameInstallationService {
|
||||
|
||||
if (pathSegments.isEmpty ||
|
||||
pathSegments.any((segment) => segment == '..') ||
|
||||
p.posix.isAbsolute(normalizedPath)) {
|
||||
pathSegments.any((segment) => segment.contains(':')) ||
|
||||
p.posix.isAbsolute(normalizedPath) ||
|
||||
p.windows.isAbsolute(normalizedPath)) {
|
||||
throw LauncherSyncException('Invalid client file path: $relativePath');
|
||||
}
|
||||
|
||||
@@ -460,28 +481,19 @@ Future<void> _scanInstallationIsolateMain(Map<String, Object?> message) async {
|
||||
final cachedHashes = (message['cachedHashes'] as Map<Object?, Object?>).map(
|
||||
(key, value) => MapEntry(key.toString(), value),
|
||||
);
|
||||
final ignoredDirectories = (message['ignoredDirectories'] as List<Object?>)
|
||||
.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()) {
|
||||
sendPort.send(<String, Object?>{'type': 'done'});
|
||||
sendPort.send(<String, Object?>{'type': 'done', 'stalePaths': <String>[]});
|
||||
return;
|
||||
}
|
||||
|
||||
final pendingFiles = <_SerializablePendingFile>[];
|
||||
await _collectFilesInIsolate(
|
||||
rootDirectory,
|
||||
rootDirectory.path,
|
||||
pendingFiles,
|
||||
ignoredDirectories,
|
||||
serverFiles,
|
||||
);
|
||||
await _collectManifestFiles(rootDirectory.path, pendingFiles, serverFiles);
|
||||
final stalePaths = await _collectStalePaths(rootDirectory, serverFiles);
|
||||
|
||||
pendingFiles.sort(
|
||||
(left, right) => left.relativePath.compareTo(right.relativePath),
|
||||
@@ -519,7 +531,7 @@ Future<void> _scanInstallationIsolateMain(Map<String, Object?> message) async {
|
||||
}
|
||||
|
||||
await _writeHashCache(hashCachePath, updatedCacheEntries);
|
||||
sendPort.send(<String, Object?>{'type': 'done'});
|
||||
sendPort.send(<String, Object?>{'type': 'done', 'stalePaths': stalePaths});
|
||||
}
|
||||
|
||||
Future<String> _resolveFileHash(
|
||||
@@ -544,26 +556,35 @@ Future<String> _resolveFileHash(
|
||||
return digest.toString();
|
||||
}
|
||||
|
||||
Future<void> _collectFilesInIsolate(
|
||||
Directory directory,
|
||||
Future<void> _collectManifestFiles(
|
||||
String rootPath,
|
||||
List<_SerializablePendingFile> pendingFiles,
|
||||
Set<String> ignoredDirectories,
|
||||
Set<String> serverFiles,
|
||||
) async {
|
||||
final installationService = GameInstallationService();
|
||||
final resolvedRoot = await Directory(rootPath).resolveSymbolicLinks();
|
||||
for (final path in serverFiles) {
|
||||
final file = File('$rootPath/$path');
|
||||
final relativePath = normalizeClientPath(path);
|
||||
final file = File(
|
||||
installationService.resolveClientPath(rootPath, relativePath),
|
||||
);
|
||||
|
||||
if (!await file.exists()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final resolvedFile = await file.resolveSymbolicLinks();
|
||||
if (!p.isWithin(resolvedRoot, resolvedFile)) {
|
||||
throw LauncherSyncException(
|
||||
'Client file points outside installation: $path',
|
||||
);
|
||||
}
|
||||
final stat = await file.stat();
|
||||
|
||||
pendingFiles.add(
|
||||
_SerializablePendingFile(
|
||||
path: file.path,
|
||||
relativePath: normalizeClientPath(p.relative(path)),
|
||||
relativePath: relativePath,
|
||||
size: stat.size,
|
||||
modifiedMs: stat.modified.millisecondsSinceEpoch,
|
||||
),
|
||||
@@ -571,6 +592,80 @@ Future<void> _collectFilesInIsolate(
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _ensureParentInsideInstallation(
|
||||
String installationDir,
|
||||
String filePath,
|
||||
) async {
|
||||
final rootPath = await Directory(installationDir).resolveSymbolicLinks();
|
||||
final parentPath = await File(filePath).parent.resolveSymbolicLinks();
|
||||
if (!p.equals(rootPath, parentPath) && !p.isWithin(rootPath, parentPath)) {
|
||||
throw LauncherSyncException(
|
||||
'Client path points outside installation: $filePath',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<String>> _collectStalePaths(
|
||||
Directory rootDirectory,
|
||||
Set<String> serverFiles,
|
||||
) async {
|
||||
String pathKey(String path) {
|
||||
final normalized = normalizeClientPath(path);
|
||||
return Platform.isWindows ? normalized.toLowerCase() : normalized;
|
||||
}
|
||||
|
||||
final manifestPaths = serverFiles.map(pathKey).toSet();
|
||||
final manifestDirectories = <String>{};
|
||||
for (final filePath in manifestPaths) {
|
||||
var directory = p.posix.dirname(filePath);
|
||||
while (directory != '.') {
|
||||
manifestDirectories.add(directory);
|
||||
directory = p.posix.dirname(directory);
|
||||
}
|
||||
}
|
||||
final stalePaths = <String>[];
|
||||
|
||||
String relativePath(FileSystemEntity entity) =>
|
||||
normalizeClientPath(p.relative(entity.path, from: rootDirectory.path));
|
||||
|
||||
Future<void> collectData(Directory directory) async {
|
||||
await for (final entity in directory.list(followLinks: false)) {
|
||||
final path = relativePath(entity);
|
||||
if (entity is Directory) {
|
||||
await collectData(entity);
|
||||
if (!manifestDirectories.contains(pathKey(path))) {
|
||||
stalePaths.add(path);
|
||||
}
|
||||
} else if (!manifestPaths.contains(pathKey(path)) &&
|
||||
!(entity is Link && manifestDirectories.contains(pathKey(path)))) {
|
||||
stalePaths.add(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> collectClientRoot(
|
||||
Directory directory, {
|
||||
bool nested = false,
|
||||
}) async {
|
||||
await for (final entity in directory.list(followLinks: false)) {
|
||||
final name = p.basename(entity.path).toLowerCase();
|
||||
if (entity is Directory) {
|
||||
if (name == 'data') {
|
||||
await collectData(entity);
|
||||
} else if (!nested && name == 'wow') {
|
||||
await collectClientRoot(entity, nested: true);
|
||||
}
|
||||
} else if ((name == 'wow.exe' || name.endsWith('.dll')) &&
|
||||
!manifestPaths.contains(pathKey(relativePath(entity)))) {
|
||||
stalePaths.add(relativePath(entity));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await collectClientRoot(rootDirectory);
|
||||
return stalePaths;
|
||||
}
|
||||
|
||||
Future<void> _writeHashCache(
|
||||
String hashCachePath,
|
||||
Map<String, Map<String, Object?>> entries,
|
||||
|
||||
Reference in New Issue
Block a user