Repository: oliverschwendener/ueli
Pull request #1412 (2025-04-14 07:57:53)

Application Search: check folder exists on Windows
Since v9 when a folder does not exist on Windows it fails to scan other folders while in v8 it was just a warning and skipped invalid paths. This might happen when for example you're using a USB drive that might not be plugged in. I think we should keep the same behavior. This is also consistent with behavior on Linux.

crazy-max: Updated to fix ci issue:

![image](https://github.com/user-attachments/assets/1fb324f1-fcd4-4948-a769-d309b6b06060)


Patch:
diff --git a/src/main/Extensions/ApplicationSearch/ApplicationSearchModule.ts b/src/main/Extensions/ApplicationSearch/ApplicationSearchModule.ts
index ee391fa8..eb36170a 100644
--- a/src/main/Extensions/ApplicationSearch/ApplicationSearchModule.ts
+++ b/src/main/Extensions/ApplicationSearch/ApplicationSearchModule.ts
@@ -32,6 +32,7 @@ export class ApplicationSearchModule implements ExtensionModule {
                 new WindowsApplicationRepository(
                     moduleRegistry.get("PowershellUtility"),
                     settings,
+                    moduleRegistry.get("FileSystemUtility"),
                     moduleRegistry.get("FileImageGenerator"),
                     moduleRegistry.get("Logger"),
                     moduleRegistry.get("AssetPathResolver"),
diff --git a/src/main/Extensions/ApplicationSearch/Windows/WindowsApplicationRepository.ts b/src/main/Extensions/ApplicationSearch/Windows/WindowsApplicationRepository.ts
index ea4214c8..b929008c 100644
--- a/src/main/Extensions/ApplicationSearch/Windows/WindowsApplicationRepository.ts
+++ b/src/main/Extensions/ApplicationSearch/Windows/WindowsApplicationRepository.ts
@@ -1,4 +1,5 @@
 import type { AssetPathResolver } from "@Core/AssetPathResolver";
+import type { FileSystemUtility } from "@Core/FileSystemUtility";
 import type { FileImageGenerator } from "@Core/ImageGenerator";
 import type { Logger } from "@Core/Logger";
 import type { PowershellUtility } from "@Core/PowershellUtility";
@@ -17,11 +18,28 @@ export class WindowsApplicationRepository implements ApplicationRepository {
     public constructor(
         private readonly powershellUtility: PowershellUtility,
         private readonly settings: Settings,
+        private readonly fileSystemUtility: FileSystemUtility,
         private readonly fileImageGenerator: FileImageGenerator,
         private readonly logger: Logger,
         private readonly assetPathResolver: AssetPathResolver,
     ) {}
 
+    private getExistingFolderPaths(candidates: string[]): string[] {
+        const existingFolderPaths: string[] = [];
+
+        for (const candidate of candidates) {
+            if (this.fileSystemUtility.isDirectory(candidate)) {
+                existingFolderPaths.push(candidate);
+            } else {
+                this.logger.warn(
+                    `Unable to get applications from folder "${candidate}". Reason: path doesn't exist or isn't a folder`,
+                );
+            }
+        }
+
+        return existingFolderPaths;
+    }
+
     public async getApplications(): Promise<Application[]> {
         const manuallyInstalledApps = await this.getManuallyInstalledApps();
         const windowsStoreApps = await this.getWindowsStoreApps();
@@ -30,7 +48,7 @@ export class WindowsApplicationRepository implements ApplicationRepository {
     }
 
     private async getManuallyInstalledApps(): Promise<WindowsApplication[]> {
-        const folderPaths = this.settings.getValue<string[]>("windowsFolders");
+        const folderPaths = this.getExistingFolderPaths(this.settings.getValue<string[]>("windowsFolders"));
         const fileExtensions = this.settings.getValue<string[]>("windowsFileExtensions");
 
         if (!folderPaths.length || !fileExtensions.length) {


Test patch:
diff --git a/src/main/Extensions/ApplicationSearch/Windows/WindowsApplicationRepository.test.ts b/src/main/Extensions/ApplicationSearch/Windows/WindowsApplicationRepository.test.ts
index 2b787aed..7dd0cb8e 100644
--- a/src/main/Extensions/ApplicationSearch/Windows/WindowsApplicationRepository.test.ts
+++ b/src/main/Extensions/ApplicationSearch/Windows/WindowsApplicationRepository.test.ts
@@ -1,5 +1,6 @@
 import type { Image } from "@common/Core/Image";
 import type { AssetPathResolver } from "@Core/AssetPathResolver";
+import type { FileSystemUtility } from "@Core/FileSystemUtility";
 import type { FileImageGenerator } from "@Core/ImageGenerator";
 import type { Logger } from "@Core/Logger";
 import type { PowershellUtility } from "@Core/PowershellUtility";
@@ -20,7 +21,7 @@ describe(WindowsApplicationRepository, () => {
             expected: WindowsApplication[];
             includeWindowsStoreApps: boolean;
         }) => {
-            const folders = ["folder1", "folder2"];
+            const folders = ["folder1", "folder2", "folder3"];
             const fileExtensions = ["ext1", "ext2"];
 
             const executeScriptMock = vi.fn().mockImplementation((s) => {
@@ -55,6 +56,12 @@ describe(WindowsApplicationRepository, () => {
 
             const fileImageGenerator = <FileImageGenerator>{ getImages: (f) => getImagesMock(f) };
 
+            const isDirectoryMock = vi.fn().mockImplementation((path) => ["folder1", "folder2"].includes(path));
+
+            const fileSystemUtility = <FileSystemUtility>{
+                isDirectory: (path) => isDirectoryMock(path),
+            };
+
             const warnMock = vi.fn();
 
             const logger = <Logger>{ warn: (m) => warnMock(m) };
@@ -68,6 +75,7 @@ describe(WindowsApplicationRepository, () => {
             const windowsApplicationRepository = new WindowsApplicationRepository(
                 powershellUtility,
                 settings,
+                fileSystemUtility,
                 fileImageGenerator,
                 logger,
                 assetPathResolver,
@@ -76,7 +84,12 @@ describe(WindowsApplicationRepository, () => {
             expect(await windowsApplicationRepository.getApplications()).toEqual(expected);
             expect(executeScriptMock).toHaveBeenCalledTimes(includeWindowsStoreApps ? 2 : 1);
             expect(getImagesMock).toHaveBeenCalledWith(["PathToApp1", "PathToApp2"]);
+            expect(isDirectoryMock).toHaveBeenCalledTimes(3);
             expect(getExtensionAssetPathMock).toHaveBeenCalledWith("ApplicationSearch", "windows-generic-app-icon.png");
+            expect(warnMock).toHaveBeenCalledTimes(2);
+            expect(warnMock).toHaveBeenCalledWith(
+                "Unable to get applications from folder \"folder3\". Reason: path doesn't exist or isn't a folder",
+            );
             expect(warnMock).toHaveBeenCalledWith(
                 'Failed to generate icon for "PathToApp2". Using generic icon instead',
             );