Implement basic file handling functions in main
This commit is contained in:
parent
920cc53ce3
commit
9096973dc6
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 () {
|
||||||
|
|
|
||||||
|
|
@ -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[]>,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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[]>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue