Implement basic file handling functions in main

This commit is contained in:
Quinten Kock 2025-10-18 01:36:22 +02:00
parent 920cc53ce3
commit 9096973dc6
4 changed files with 300 additions and 3 deletions

View File

@ -13,6 +13,12 @@ type FolderTree = {
children?: FolderTree[]; 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<string>();
async function readTree(dirPath: string): Promise<FolderTree[]> { async function readTree(dirPath: string): Promise<FolderTree[]> {
const stats = await fsp.stat(dirPath); const stats = await fsp.stat(dirPath);
if (!stats.isDirectory()) return []; if (!stats.isDirectory()) return [];
@ -35,7 +41,7 @@ async function readTree(dirPath: string): Promise<FolderTree[]> {
type: "file" as const, type: "file" as const,
}; };
} }
}) }),
); );
return children; return children;
} }
@ -48,6 +54,7 @@ export async function handleOpenFolder(
}); });
if (!result.canceled && result.filePaths.length > 0) { if (!result.canceled && result.filePaths.length > 0) {
const folderPath = result.filePaths[0]; const folderPath = result.filePaths[0];
currentWorkspaceRoot = folderPath; // Track the opened folder
return { return {
name: path.basename(folderPath), name: path.basename(folderPath),
path: folderPath, path: folderPath,
@ -57,3 +64,217 @@ export async function handleOpenFolder(
} }
return null; 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);
}

View File

@ -1,5 +1,12 @@
import { app, BrowserWindow, ipcMain } from "electron"; import { app, BrowserWindow, ipcMain } from "electron";
import { handleOpenFolder } from "./fileOperations"; import {
handleOpenFolder,
handleReadFile,
handleSaveFile,
// handleCreateFile,
getCurrentWorkspace,
getOpenedFiles,
} from "./fileOperations";
import path from "node:path"; import path from "node:path";
import started from "electron-squirrel-startup"; import started from "electron-squirrel-startup";
@ -44,6 +51,34 @@ app.whenReady().then(() => {
const senderWindow = BrowserWindow.fromWebContents(event.sender); const senderWindow = BrowserWindow.fromWebContents(event.sender);
return await handleOpenFolder(senderWindow); return await handleOpenFolder(senderWindow);
}); });
// File operation handlers
ipcMain.handle("file:read", async (event, filePath?: string) => {
const senderWindow = BrowserWindow.fromWebContents(event.sender);
return await handleReadFile(senderWindow, filePath);
});
ipcMain.handle(
"file:save",
async (event, content: string, filePath?: string) => {
const senderWindow = BrowserWindow.fromWebContents(event.sender);
return await handleSaveFile(senderWindow, content, filePath);
},
);
// ipcMain.handle("file:create", async (event, fileName: string, content = '', directory?: string) => {
// const senderWindow = BrowserWindow.fromWebContents(event.sender);
// return await handleCreateFile(senderWindow, fileName, content, directory);
// });
ipcMain.handle("workspace:getCurrentInfo", () => {
return getCurrentWorkspace();
});
ipcMain.handle("workspace:getOpenedFiles", () => {
return getOpenedFiles();
});
createWindow(); createWindow();
if (process.platform === "darwin") { if (process.platform === "darwin") {
app.on("activate", function () { app.on("activate", function () {

View File

@ -7,4 +7,32 @@ import type { FolderTree } from "./types/global";
contextBridge.exposeInMainWorld("electronAPI", { contextBridge.exposeInMainWorld("electronAPI", {
openFolder: () => openFolder: () =>
ipcRenderer.invoke("dialog:openFolder") as Promise<FolderTree | null>, ipcRenderer.invoke("dialog:openFolder") as Promise<FolderTree | null>,
// File operations
readFile: (filePath?: string) =>
ipcRenderer.invoke("file:read", filePath) as Promise<{
content: string;
path: string;
} | null>,
saveFile: (content: string, filePath?: string) =>
ipcRenderer.invoke("file:save", content, filePath) as Promise<{
path: string;
} | null>,
// createFile: (fileName: string, content = "", directory?: string) =>
// ipcRenderer.invoke(
// "file:create",
// fileName,
// content,
// directory,
// ) as Promise<{ path: string } | null>,
getCurrentWorkspace: () =>
ipcRenderer.invoke("workspace:getCurrentInfo") as Promise<{
root: string | null;
}>,
getOpenedFiles: () =>
ipcRenderer.invoke("workspace:getOpenedFiles") as Promise<string[]>,
}); });

15
src/types/global.d.ts vendored
View File

@ -13,7 +13,20 @@ declare global {
interface Window { interface Window {
electronAPI: { electronAPI: {
openFolder: () => Promise<FolderTree | null>; openFolder: () => Promise<FolderTree | null>;
// Add other methods as needed
// File operations
readFile: (
filePath?: string,
) => Promise<{ content: string; path: string } | null>;
saveFile: (
content: string,
filePath?: string,
) => Promise<{ path: string } | null>;
// createFile: (fileName: string, content?: string, directory?: string) => Promise<{ path: string } | null>;
// Workspace info
getCurrentWorkspace: () => Promise<{ root: string | null }>;
getOpenedFiles: () => Promise<string[]>;
}; };
} }
} }