Clean obsolete Extentions during client synchronization

Include the prepared cleanup of both client roots while preserving manifest-listed files and unrelated user state. Verified on Windows through FVM: full formatting and analysis, all 76 tests, production dry run, x64 build and DSA signature verification.
This commit is contained in:
2026-10-10 16:36:51 +04:00
parent c802839688
commit 7799b33239
6 changed files with 43 additions and 8 deletions
+2
View File
@@ -96,6 +96,8 @@ or local-only API URLs.
- The server manifest is the source of truth for client files.
- Sync removes files absent from the manifest under `Data` and `WoW/Data`,
plus legacy `WoW.exe` and DLLs directly in the installation root or `WoW`.
Also clean obsolete `Extentions` directories in either client root and remove
the directory itself when no manifest files require it.
Remove unused Data subdirectories only after their children, without recursive
deletion. Preserve unrelated files and user state outside these areas.
- Language archives under `Data/ruRU` or `WoW/Data/ruRU` use the regular manifest sync.
+2
View File
@@ -28,6 +28,8 @@ During synchronization, the manifest is authoritative for `Data/` and
Legacy `WoW.exe` and DLLs in the installation root or `WoW/` are also removed
when absent from the manifest. Required DLLs stay; player settings, addons,
logs, screenshots, and other files outside these cleanup areas are preserved.
The obsolete `Extentions/` directory in either client root is cleaned the same
way and removed when no manifest files require it.
Cleanup runs even when all manifest files are already up to date.
## Stack
+5 -1
View File
@@ -173,6 +173,9 @@ to the selected installation root, including files in nested directories.
The scan separately collects stale paths without hashing their contents:
- every file absent from the manifest in `Data/` and `WoW/Data/`
- every file absent from the manifest in obsolete `Extentions/` and
`WoW/Extentions/` directories, including unused subdirectories and the
Extentions directory itself when no manifest files require it
- `WoW.exe` and `*.dll` absent from the manifest directly in the installation
root or `WoW/` (binary names are matched without case sensitivity)
- unused Data subdirectories, ordered after their children; the Data roots and
@@ -325,7 +328,8 @@ The automated tests cover:
- deterministic `buildHash`
- path normalization used by `buildHash`
- preservation of user state and unrelated unmanaged files during cleanup
- removal of unmanaged Data files, unused subdirectories, WoW.exe, and DLLs
- removal of unmanaged Data files, unused subdirectories, WoW.exe, DLLs, and
obsolete Extentions directories
- preservation of manifest-listed DLLs and WoW.exe
- cache cleanup behavior
- safe path resolution
@@ -630,11 +630,11 @@ Future<List<String>> _collectStalePaths(
String relativePath(FileSystemEntity entity) =>
normalizeClientPath(p.relative(entity.path, from: rootDirectory.path));
Future<void> collectData(Directory directory) async {
Future<void> collectStaleDirectory(Directory directory) async {
await for (final entity in directory.list(followLinks: false)) {
final path = relativePath(entity);
if (entity is Directory) {
await collectData(entity);
await collectStaleDirectory(entity);
if (!manifestDirectories.contains(pathKey(path))) {
stalePaths.add(path);
}
@@ -653,12 +653,20 @@ Future<List<String>> _collectStalePaths(
final name = p.basename(entity.path).toLowerCase();
if (entity is Directory) {
if (name == 'data') {
await collectData(entity);
await collectStaleDirectory(entity);
} else if (name == 'extentions') {
await collectStaleDirectory(entity);
if (!manifestDirectories.contains(pathKey(relativePath(entity)))) {
stalePaths.add(relativePath(entity));
}
} else if (!nested && name == 'wow') {
await collectClientRoot(entity, nested: true);
}
} else if ((name == 'wow.exe' || name.endsWith('.dll')) &&
!manifestPaths.contains(pathKey(relativePath(entity)))) {
} else if ((name == 'wow.exe' ||
name.endsWith('.dll') ||
(entity is Link && name == 'extentions')) &&
!manifestPaths.contains(pathKey(relativePath(entity))) &&
!manifestDirectories.contains(pathKey(relativePath(entity)))) {
stalePaths.add(relativePath(entity));
}
}
@@ -169,7 +169,12 @@ void main() {
addTearDown(() => root.delete(recursive: true));
final service = GameInstallationService();
final retained = ['MoonWell.exe', 'Data/common.MPQ', 'required.dll'];
final obsolete = ['Data/old/patch.MPQ', 'WoW.exe', 'unused.DLL'];
final obsolete = [
'Data/old/patch.MPQ',
'WoW.exe',
'unused.DLL',
'Extentions/bin/extension.dll',
];
for (final path in [
...retained,
...obsolete,
@@ -217,7 +222,12 @@ void main() {
contains(ClientSyncStage.removingStaleFiles),
);
expect(api.downloadedPaths, isEmpty);
for (final path in [...obsolete, 'Data/old']) {
for (final path in [
...obsolete,
'Data/old',
'Extentions/bin',
'Extentions',
]) {
expect(
await FileSystemEntity.type(
service.resolveClientPath(root.path, path),
@@ -77,6 +77,8 @@ void main() {
'WoW/WoW.exe',
'WoW/old.dll',
'WoW/Data/old.MPQ',
'Extentions/bin/extension.dll',
'WoW/Extentions/old.txt',
];
final untouchedPaths = [
'Cache/cache.bin',
@@ -105,6 +107,9 @@ void main() {
await Directory(
service.resolveClientPath(rootDirectory.path, 'Data/unused/nested'),
).create(recursive: true);
await Directory(
service.resolveClientPath(rootDirectory.path, 'Extentions/empty'),
).create(recursive: true);
final manifest = await _manifestForPaths(
service,
rootDirectory,
@@ -129,6 +134,10 @@ void main() {
'Data/empty',
'Data/unused/nested',
'Data/unused',
'Extentions/bin',
'Extentions/empty',
'Extentions',
'WoW/Extentions',
]),
);
for (final path in snapshot.stalePaths) {