// src/main/fileOperations.ts // Handles file operations for the main process import { dialog, BrowserWindow } from "electron"; import fs from "fs"; const fsp = fs.promises; import path from "path"; type FolderTree = { name: string; path: string; type: "directory" | "file"; children?: FolderTree[]; }; // Track the currently opened folder for security checks let currentWorkspaceRoot: string | null = null; // Track previously opened files outside the workspace const openedFiles = new Set(); async function readTree(dirPath: string): Promise { const stats = await fsp.stat(dirPath); if (!stats.isDirectory()) return []; const names = await fsp.readdir(dirPath); const children = await Promise.all( names.map(async (name) => { const fullPath = path.join(dirPath, name); const stat = await fsp.stat(fullPath); if (stat.isDirectory()) { return { name, path: fullPath, type: "directory" as const, children: await readTree(fullPath), }; } else { return { name, path: fullPath, type: "file" as const, }; } }), ); return children; } export async function handleOpenFolder( mainWindow: BrowserWindow, ): Promise { const result = await dialog.showOpenDialog(mainWindow, { properties: ["openDirectory"], }); if (!result.canceled && result.filePaths.length > 0) { const folderPath = result.filePaths[0]; currentWorkspaceRoot = folderPath; // Track the opened folder return { name: path.basename(folderPath), path: folderPath, type: "directory", children: await readTree(folderPath), }; } return null; } // Security helper: Check if a path is within the current workspace or previously opened function isPathSecure(filePath: string): boolean { const resolvedPath = path.resolve(filePath); // Allow if within workspace if (currentWorkspaceRoot) { const resolvedRoot = path.resolve(currentWorkspaceRoot); if ( resolvedPath.startsWith(resolvedRoot + path.sep) || resolvedPath === resolvedRoot ) { return true; } } // Allow if previously opened if (openedFiles.has(resolvedPath)) { return true; } return false; } // File Operations export async function handleReadFile( mainWindow: BrowserWindow, filePath?: string, ): Promise<{ content: string; path: string } | null> { let targetPath: string; if (filePath) { // If path is provided, check if it's secure if (!isPathSecure(filePath)) { throw new Error( "Access denied: File is outside the workspace and not previously opened", ); } targetPath = filePath; } else { // Show file dialog const result = await dialog.showOpenDialog(mainWindow, { properties: ["openFile"], defaultPath: currentWorkspaceRoot || undefined, // filters: [ // { name: "All Files", extensions: ["*"] }, // { // name: "Text Files", // extensions: [ // "txt", // "md", // "js", // "ts", // "json", // "html", // "css", // ], // }, // ], }); if (result.canceled || result.filePaths.length === 0) { return null; } targetPath = result.filePaths[0]; // Track files opened via dialog to allow future access openedFiles.add(path.resolve(targetPath)); } try { const content = await fsp.readFile(targetPath, "utf8"); return { content, path: targetPath }; } catch (error) { throw new Error(`Failed to read file: ${error.message}`); } } export async function handleSaveFile( mainWindow: BrowserWindow, content: string, filePath?: string, ): Promise<{ path: string } | null> { let targetPath: string; if (filePath) { // If path is provided, check if it's secure if (!isPathSecure(filePath)) { throw new Error( "Access denied: File is outside the workspace and not previously opened", ); } targetPath = filePath; } else { // Show save dialog const result = await dialog.showSaveDialog(mainWindow, { defaultPath: currentWorkspaceRoot || undefined, // filters: [ // { name: "All Files", extensions: ["*"] }, // { // name: "Text Files", // extensions: [ // "txt", // "md", // "js", // "ts", // "json", // "html", // "css", // ], // }, // ], }); if (result.canceled || !result.filePath) { return null; } targetPath = result.filePath; // Track files saved via dialog (always allowed) openedFiles.add(path.resolve(targetPath)); } try { // Ensure directory exists const dir = path.dirname(targetPath); await fsp.mkdir(dir, { recursive: true }); await fsp.writeFile(targetPath, content, "utf8"); return { path: targetPath }; } catch (error) { throw new Error(`Failed to save file: ${error.message}`); } } // export async function handleCreateFile( // mainWindow: BrowserWindow, // fileName: string, // content = "", // directory?: string, // ): Promise<{ path: string } | null> { // let targetDir: string; // if (directory) { // // If directory is provided, check if it's secure // if (!isPathSecure(directory)) { // throw new Error( // "Access denied: Directory is outside the workspace", // ); // } // targetDir = directory; // } else if (currentWorkspaceRoot) { // // Use current workspace root // targetDir = currentWorkspaceRoot; // } else { // // Show directory selection dialog // const result = await dialog.showOpenDialog(mainWindow, { // properties: ["openDirectory"], // title: "Select directory to create file in", // }); // if (result.canceled || result.filePaths.length === 0) { // return null; // } // targetDir = result.filePaths[0]; // } // const targetPath = path.join(targetDir, fileName); // // Double-check the final path is secure // if (!isPathSecure(targetPath)) { // throw new Error("Access denied: Target path is outside the workspace"); // } // try { // // Check if file already exists // const exists = await fsp // .access(targetPath) // .then(() => true) // .catch(() => false); // if (exists) { // // Ask user if they want to overwrite // const choice = await dialog.showMessageBox(mainWindow, { // type: "question", // buttons: ["Cancel", "Overwrite"], // defaultId: 0, // message: `File "${fileName}" already exists. Do you want to overwrite it?`, // }); // if (choice.response === 0) { // return null; // User cancelled // } // } // await fsp.writeFile(targetPath, content, "utf8"); // return { path: targetPath }; // } catch (error) { // throw new Error(`Failed to create file: ${error.message}`); // } // } // Utility function to get current workspace info export function getCurrentWorkspace(): { root: string | null } { return { root: currentWorkspaceRoot }; } // Utility function to get opened files (for debugging/info) export function getOpenedFiles(): string[] { return Array.from(openedFiles); } // Show confirmation dialog export async function showConfirmDialog( mainWindow: BrowserWindow, message: string, title: string, buttons: string[] = ["OK", "Cancel"], ): Promise { const result = await dialog.showMessageBox(mainWindow, { type: "question", buttons, defaultId: 0, title, message, }); return buttons[result.response]; }