miller/src/main/fileOperations.ts

298 lines
8.7 KiB
TypeScript

// 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<string>();
async function readTree(dirPath: string): Promise<FolderTree[]> {
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<FolderTree | null> {
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<string> {
const result = await dialog.showMessageBox(mainWindow, {
type: "question",
buttons,
defaultId: 0,
title,
message,
});
return buttons[result.response];
}