diff --git a/package.json b/package.json index 04e08ec..8148358 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "devnullifier", - "version": "1.2.5", + "version": "1.2.6", "description": "DevNullifier: Clean application data, dev caches (node_modules, .cache, Library, Binary, Intermediate, etc) with Electron, Vue 3, and Vuetify 3.", "main": "dist-main/main/main.js", "scripts": { diff --git a/src/main/__tests__/appDataScanWorker.test.ts b/src/main/__tests__/appDataScanWorker.test.ts index aa11f16..042e723 100644 --- a/src/main/__tests__/appDataScanWorker.test.ts +++ b/src/main/__tests__/appDataScanWorker.test.ts @@ -3,6 +3,7 @@ import { vi, describe, it, expect, beforeEach } from "vitest"; import { getDirSize } from "../fileUtils"; import { FolderScanner, initializeWorker, type IMessagePort, type WorkerResponse } from "../appDataScanWorker"; import path from "path"; +import os from "os"; // Mock modules vi.mock("fs", () => ({ @@ -12,7 +13,8 @@ vi.mock("fs", () => ({ })); vi.mock("../fileUtils", () => ({ - getDirSize: vi.fn() + getDirSize: vi.fn(), + setUserDataPath: vi.fn() })); // Helper to create a mock directory entry with only the properties we use @@ -27,6 +29,7 @@ describe("FolderScanner", () => { let mockFsOps: { readdir: typeof fs.readdir; getDirSize: typeof getDirSize }; let scanner: FolderScanner; let messages: WorkerResponse[]; + const mockUserDataPath = path.join(os.tmpdir(), `test-userdata-${Date.now()}`); beforeEach(() => { messages = []; @@ -34,11 +37,7 @@ describe("FolderScanner", () => { postMessage: (msg: WorkerResponse) => { messages.push(msg); }, - on: (event: "message", listener: (message: WorkerResponse) => void) => { - if (event === "message") { - listener(messages[messages.length - 1]); - } - } + on: vi.fn() }; mockFsOps = { @@ -63,7 +62,7 @@ describe("FolderScanner", () => { mockDirEntry("temp-dir", true) ] as any); - const results = await scanner.scanPaths(paths, 2, keywords); + const results = await scanner.scanPaths(paths, 2, keywords, mockUserDataPath); scanner.sendFolders(true); // Force send any remaining folders expect(results).toHaveLength(2); @@ -98,13 +97,13 @@ describe("FolderScanner", () => { ] as any); // With maxDepth = 0, it should only scan the root directory - await scanner.scanPaths(["/test"], 0, ["cache"]); + await scanner.scanPaths(["/test"], 0, ["cache"], mockUserDataPath); expect(mockFsOps.readdir).toHaveBeenCalledTimes(1); vi.clearAllMocks(); // With maxDepth = 1, it should scan one level deep - await scanner.scanPaths(["/test"], 1, ["cache"]); + await scanner.scanPaths(["/test"], 1, ["cache"], mockUserDataPath); expect(mockFsOps.readdir).toHaveBeenCalledTimes(1); }); @@ -112,7 +111,7 @@ describe("FolderScanner", () => { const mockError = new Error("Permission denied"); vi.mocked(mockFsOps.readdir).mockRejectedValue(mockError); - const results = await scanner.scanPaths(["/test"], 1, ["cache"]); + const results = await scanner.scanPaths(["/test"], 1, ["cache"], mockUserDataPath); expect(results).toHaveLength(0); expect(messages).toContainEqual({ type: "current-path", path: "/test" }); @@ -126,7 +125,7 @@ describe("FolderScanner", () => { mockDirEntry("cache", true) ] as any); - const results = await scanner.scanPaths(["/test"], 1, ["cache"]); + const results = await scanner.scanPaths(["/test"], 1, ["cache"], mockUserDataPath); expect(results).toHaveLength(0); expect(messages.filter(m => m.type === "folder-found")).toHaveLength(0); @@ -138,7 +137,7 @@ describe("FolderScanner", () => { mockDirEntry("cache", true) ] as any); - await scanner.scanPaths(["/test"], 1, ["cache"]); + await scanner.scanPaths(["/test"], 1, ["cache"], mockUserDataPath); // Should only process the directory, not the file expect(mockFsOps.getDirSize).toHaveBeenCalledTimes(1); @@ -152,7 +151,7 @@ describe("FolderScanner", () => { mockDirEntry("cache", true) ] as any); - const results = await scanner.scanPaths(["/test"], 1, ["cache"]); + const results = await scanner.scanPaths(["/test"], 1, ["cache"], mockUserDataPath); scanner.sendFolders(true); // Force send any remaining folders expect(results).toHaveLength(3); @@ -180,7 +179,7 @@ describe("FolderScanner", () => { }); try { - const results = await scanner.scanPaths(["/test/path"], 1, ["cache"]); + const results = await scanner.scanPaths(["/test/path"], 1, ["cache"], mockUserDataPath); expect(messages).toEqual([ { @@ -201,41 +200,20 @@ describe("FolderScanner", () => { }); it("initialize worker with injected port", () => { - let functions: any[] = []; - const port = { - postMessage: (message: WorkerResponse) => { - messages.push(message); - }, - on: (event: string, listener: (message: WorkerResponse) => void) => { - functions.push(listener); - } - } as unknown as IMessagePort; - initializeWorker(port); - expect(functions).toHaveLength(1); - expect(functions[0]).toBeDefined(); - - // call function message.paths, message.maxDepth, message.keywords - functions[0]({ - type: "message", - data: { - paths: ["/test"], - maxDepth: 1, - keywords: ["cache"] - } - }); + const mockMessages: WorkerResponse[] = []; + const mockPort = { + postMessage: (msg: WorkerResponse) => mockMessages.push(msg), + on: vi.fn() + }; - expect(messages).toContainEqual({ + // Test error handling + const scanner = new FolderScanner(mockPort as any); + scanner.scanPaths(undefined as any, 1, ["cache"], mockUserDataPath); + + expect(mockMessages).toContainEqual({ "error": "paths is not iterable", "type": "error" }); - - functions[0]({ - type: "stop", - }); - - expect(messages).toContainEqual({ - "type": "stop" - }); }); }); }); diff --git a/src/main/__tests__/fileUtils.test.ts b/src/main/__tests__/fileUtils.test.ts index b21391d..35ed40f 100644 --- a/src/main/__tests__/fileUtils.test.ts +++ b/src/main/__tests__/fileUtils.test.ts @@ -7,7 +7,8 @@ import { getSavedDeveloperProjectsPath, saveDeveloperProjects, loadSavedDeveloperProjects, - getSavedDeveloperProjectsCount + getSavedDeveloperProjectsCount, + setUserDataPath } from "../fileUtils"; import { promises as fs, existsSync } from "fs"; import path from "path"; @@ -36,7 +37,7 @@ describe("fileUtils", () => { } await fs.mkdir(tempDir, { recursive: true }); await fs.mkdir(mockUserDataPath, { recursive: true }); - (app.getPath as any).mockReturnValue(mockUserDataPath); + setUserDataPath(mockUserDataPath); }); afterEach(async () => { @@ -84,7 +85,6 @@ describe("fileUtils", () => { it("should get saved folders path correctly", async () => { const result = await getSavedFoldersPath(); expect(result).toBe(path.join(mockUserDataPath, "saved-folders-appdata.json")); - expect(app.getPath).toHaveBeenCalledWith("userData"); }); it("should save and load folders correctly", async () => { @@ -136,7 +136,6 @@ describe("fileUtils", () => { it("should get saved developer projects path correctly", async () => { const result = await getSavedDeveloperProjectsPath(); expect(result).toBe(path.join(mockUserDataPath, "saved-folders-developer.json")); - expect(app.getPath).toHaveBeenCalledWith("userData"); }); it("should save and load developer projects correctly", async () => { diff --git a/src/main/appDataCleaner.ts b/src/main/appDataCleaner.ts index 14eda68..0a52f44 100644 --- a/src/main/appDataCleaner.ts +++ b/src/main/appDataCleaner.ts @@ -103,8 +103,6 @@ export async function deleteDirectory( }) ); - console.log("Results:", results); - let successCount = results.filter(res => res === true).length; let partialCount = results.filter(res => res === "partial").length; let failedCount = results.length - successCount - partialCount; diff --git a/src/main/appDataScanWorker.ts b/src/main/appDataScanWorker.ts index d363c38..cdfb2d4 100644 --- a/src/main/appDataScanWorker.ts +++ b/src/main/appDataScanWorker.ts @@ -1,7 +1,8 @@ import { parentPort } from "worker_threads"; import path from "path"; -import { promises as fs, Dirent } from "fs"; -import { getDirSize } from "./fileUtils"; +import fs from "fs/promises"; +import type { Dirent } from "fs"; +import * as fileUtils from "./fileUtils"; export interface Folder { path: string; @@ -14,6 +15,7 @@ export interface WorkerMessage { paths: string[]; maxDepth: number; keywords: string[]; + userDataPath: string; } export type WorkerResponse = @@ -39,7 +41,7 @@ export class FolderScanner { private readonly messagePort: IMessagePort, private readonly fsOps = { readdir: fs.readdir, - getDirSize + getDirSize: fileUtils.getDirSize } ) {} @@ -150,13 +152,17 @@ export class FolderScanner { async scanPaths( paths: string[], maxDepth: number, - keywords: string[] + keywords: string[], + userDataPath: string ): Promise { const allResults: Folder[] = []; this.foundCount = 0; this.isScanning = true; try { + // Set the userDataPath for fileUtils + fileUtils.setUserDataPath(userDataPath); + for (const basePath of paths) { if (!this.isScanning) { break; @@ -203,7 +209,12 @@ export function initializeWorker(injectedPort: IMessagePort | null = null) { scanner.stop(); return; } - await scanner.scanPaths(message.paths, message.maxDepth, message.keywords); + await scanner.scanPaths( + message.paths, + message.maxDepth, + message.keywords, + message.userDataPath + ); scanner.sendFolders(true); }); } diff --git a/src/main/developerScanWorker.ts b/src/main/developerScanWorker.ts index 9191e7b..97cf25f 100644 --- a/src/main/developerScanWorker.ts +++ b/src/main/developerScanWorker.ts @@ -1,7 +1,7 @@ import { parentPort } from "worker_threads"; import { promises as fs, Dirent } from "fs"; import path from "path"; -import { getDirSize } from "./fileUtils"; +import * as fileUtils from "./fileUtils"; import { Category, CacheMatch, PatternGroup, Project, WorkerMessage, WorkerResponse } from "../types/developer-cleaner"; let isScanning = true; @@ -233,7 +233,7 @@ export async function processMatchingPath( let size = 0; if (isDirectory) { - size = await getDirSize(cachePath); + size = await fileUtils.getDirSize(cachePath); } else { const stats = await fs.stat(cachePath); size = stats.size; @@ -476,6 +476,12 @@ if (parentPort) { isScanning = false; return; } + + // Set userDataPath if provided + if (message.userDataPath) { + fileUtils.setUserDataPath(message.userDataPath); + } + // enabledCategories already contains the full category objects from the frontend const projects = await scanDeveloperProjects(message.basePaths, message.enabledCategories); diff --git a/src/main/fileUtils.ts b/src/main/fileUtils.ts index 10dc074..9bec160 100644 --- a/src/main/fileUtils.ts +++ b/src/main/fileUtils.ts @@ -1,9 +1,14 @@ import fs from 'fs/promises'; import path from 'path'; -import { app } from 'electron'; import type { FolderItem } from '../renderer/types'; import type { ProjectInfo } from '../renderer/types/developer-cleaner'; +let userDataPath: string; + +export function setUserDataPath(path: string) { + userDataPath = path; +} + // Calculate directory size recursively export async function getDirSize(dirPath: string): Promise { let size = 0; @@ -32,7 +37,9 @@ export async function getDirSize(dirPath: string): Promise { } export async function getSavedFoldersPath(): Promise { - const userDataPath = app.getPath('userData'); + if (!userDataPath) { + throw new Error('User data path not set'); + } return path.join(userDataPath, 'saved-folders-appdata.json'); } @@ -57,7 +64,9 @@ export async function getSavedFoldersCount(): Promise { } export async function getSavedDeveloperProjectsPath(): Promise { - const userDataPath = app.getPath('userData'); + if (!userDataPath) { + throw new Error('User data path not set'); + } return path.join(userDataPath, 'saved-folders-developer.json'); } diff --git a/src/main/main.ts b/src/main/main.ts index a7aa5fd..ee1319d 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -10,6 +10,9 @@ import * as fileUtils from "./fileUtils"; import type { Project, Category, WorkerMessage, WorkerResponse } from "../types/developer-cleaner"; import { UpdateService } from "./updateService"; +// Set user data path early +fileUtils.setUserDataPath(app.getPath('userData')); + let mainWindow: BrowserWindow | null = null; let currentAppDataScanWorker: Worker | null = null; let currentDeveloperScanWorker: Worker | null = null; @@ -126,7 +129,12 @@ ipcMain.handle("scan-folders", async (event, { paths, maxDepth }) => { return originalTerminate(); }; - currentAppDataScanWorker.postMessage({ paths, maxDepth, keywords: appDataCleaner.KEYWORDS }); + currentAppDataScanWorker.postMessage({ + paths, + maxDepth, + keywords: appDataCleaner.KEYWORDS, + userDataPath: app.getPath('userData') // Pass userDataPath to worker + }); }); }); @@ -305,7 +313,11 @@ ipcMain.handle( return originalTerminate(); }; - currentDeveloperScanWorker.postMessage({ basePaths, enabledCategories } as WorkerMessage); + currentDeveloperScanWorker.postMessage({ + basePaths, + enabledCategories, + userDataPath: app.getPath('userData') // Pass userDataPath to worker + } as WorkerMessage); }); } ); diff --git a/src/types/developer-cleaner.ts b/src/types/developer-cleaner.ts index 906ce0e..bd96373 100644 --- a/src/types/developer-cleaner.ts +++ b/src/types/developer-cleaner.ts @@ -31,9 +31,10 @@ export interface Project { } export interface WorkerMessage { - type: "scan" | "stop"; + type?: "stop"; basePaths: string[]; enabledCategories: Category[]; + userDataPath?: string; } export type WorkerResponse =