diff --git a/AGENTS.md b/AGENTS.md index fa5e116..61a3ec8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/README.md b/README.md index ddf7b15..e31ad74 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/launcher_web_api_spec.md b/docs/launcher_web_api_spec.md index f55cbe1..196c762 100644 --- a/docs/launcher_web_api_spec.md +++ b/docs/launcher_web_api_spec.md @@ -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 diff --git a/lib/features/launcher/data/game_installation_service.dart b/lib/features/launcher/data/game_installation_service.dart index 6c5061c..aace1e6 100644 --- a/lib/features/launcher/data/game_installation_service.dart +++ b/lib/features/launcher/data/game_installation_service.dart @@ -630,11 +630,11 @@ Future> _collectStalePaths( String relativePath(FileSystemEntity entity) => normalizeClientPath(p.relative(entity.path, from: rootDirectory.path)); - Future collectData(Directory directory) async { + Future 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> _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)); } } diff --git a/test/features/launcher/application/client_sync_use_case_test.dart b/test/features/launcher/application/client_sync_use_case_test.dart index b54f5fe..7032bc1 100644 --- a/test/features/launcher/application/client_sync_use_case_test.dart +++ b/test/features/launcher/application/client_sync_use_case_test.dart @@ -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), diff --git a/test/features/launcher/data/game_installation_service_test.dart b/test/features/launcher/data/game_installation_service_test.dart index 9c37809..985920a 100644 --- a/test/features/launcher/data/game_installation_service_test.dart +++ b/test/features/launcher/data/game_installation_service_test.dart @@ -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) {