mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 20:23:36 +00:00
- Removed unused Electron API methods and simplified the main process. - Introduced a new workspace picker modal for improved project selection. - Enhanced error handling for authentication issues across various components. - Updated UI styles for dark mode support and added new CSS variables. - Refactored session management to utilize a centralized API access method. - Added server routes for workspace management, including directory listing and configuration checks.
320 lines
9.5 KiB
TypeScript
320 lines
9.5 KiB
TypeScript
/**
|
|
* File system routes
|
|
* Provides REST API equivalents for Electron IPC file operations
|
|
*/
|
|
|
|
import { Router, type Request, type Response } from "express";
|
|
import fs from "fs/promises";
|
|
import path from "path";
|
|
import { validatePath, addAllowedPath, isPathAllowed } from "../lib/security.js";
|
|
import type { EventEmitter } from "../lib/events.js";
|
|
|
|
export function createFsRoutes(_events: EventEmitter): Router {
|
|
const router = Router();
|
|
|
|
// Read file
|
|
router.post("/read", async (req: Request, res: Response) => {
|
|
try {
|
|
const { filePath } = req.body as { filePath: string };
|
|
|
|
if (!filePath) {
|
|
res.status(400).json({ success: false, error: "filePath is required" });
|
|
return;
|
|
}
|
|
|
|
const resolvedPath = validatePath(filePath);
|
|
const content = await fs.readFile(resolvedPath, "utf-8");
|
|
|
|
res.json({ success: true, content });
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
res.status(500).json({ success: false, error: message });
|
|
}
|
|
});
|
|
|
|
// Write file
|
|
router.post("/write", async (req: Request, res: Response) => {
|
|
try {
|
|
const { filePath, content } = req.body as {
|
|
filePath: string;
|
|
content: string;
|
|
};
|
|
|
|
if (!filePath) {
|
|
res.status(400).json({ success: false, error: "filePath is required" });
|
|
return;
|
|
}
|
|
|
|
const resolvedPath = validatePath(filePath);
|
|
|
|
// Ensure parent directory exists
|
|
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
|
|
await fs.writeFile(resolvedPath, content, "utf-8");
|
|
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
res.status(500).json({ success: false, error: message });
|
|
}
|
|
});
|
|
|
|
// Create directory
|
|
router.post("/mkdir", async (req: Request, res: Response) => {
|
|
try {
|
|
const { dirPath } = req.body as { dirPath: string };
|
|
|
|
if (!dirPath) {
|
|
res.status(400).json({ success: false, error: "dirPath is required" });
|
|
return;
|
|
}
|
|
|
|
const resolvedPath = validatePath(dirPath);
|
|
await fs.mkdir(resolvedPath, { recursive: true });
|
|
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
res.status(500).json({ success: false, error: message });
|
|
}
|
|
});
|
|
|
|
// Read directory
|
|
router.post("/readdir", async (req: Request, res: Response) => {
|
|
try {
|
|
const { dirPath } = req.body as { dirPath: string };
|
|
|
|
if (!dirPath) {
|
|
res.status(400).json({ success: false, error: "dirPath is required" });
|
|
return;
|
|
}
|
|
|
|
const resolvedPath = validatePath(dirPath);
|
|
const entries = await fs.readdir(resolvedPath, { withFileTypes: true });
|
|
|
|
const result = entries.map((entry) => ({
|
|
name: entry.name,
|
|
isDirectory: entry.isDirectory(),
|
|
isFile: entry.isFile(),
|
|
}));
|
|
|
|
res.json({ success: true, entries: result });
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
res.status(500).json({ success: false, error: message });
|
|
}
|
|
});
|
|
|
|
// Check if file/directory exists
|
|
router.post("/exists", async (req: Request, res: Response) => {
|
|
try {
|
|
const { filePath } = req.body as { filePath: string };
|
|
|
|
if (!filePath) {
|
|
res.status(400).json({ success: false, error: "filePath is required" });
|
|
return;
|
|
}
|
|
|
|
// For exists, we check but don't require the path to be pre-allowed
|
|
// This allows the UI to validate user-entered paths
|
|
const resolvedPath = path.resolve(filePath);
|
|
|
|
try {
|
|
await fs.access(resolvedPath);
|
|
res.json({ success: true, exists: true });
|
|
} catch {
|
|
res.json({ success: true, exists: false });
|
|
}
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
res.status(500).json({ success: false, error: message });
|
|
}
|
|
});
|
|
|
|
// Get file stats
|
|
router.post("/stat", async (req: Request, res: Response) => {
|
|
try {
|
|
const { filePath } = req.body as { filePath: string };
|
|
|
|
if (!filePath) {
|
|
res.status(400).json({ success: false, error: "filePath is required" });
|
|
return;
|
|
}
|
|
|
|
const resolvedPath = validatePath(filePath);
|
|
const stats = await fs.stat(resolvedPath);
|
|
|
|
res.json({
|
|
success: true,
|
|
stats: {
|
|
isDirectory: stats.isDirectory(),
|
|
isFile: stats.isFile(),
|
|
size: stats.size,
|
|
mtime: stats.mtime,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
res.status(500).json({ success: false, error: message });
|
|
}
|
|
});
|
|
|
|
// Delete file
|
|
router.post("/delete", async (req: Request, res: Response) => {
|
|
try {
|
|
const { filePath } = req.body as { filePath: string };
|
|
|
|
if (!filePath) {
|
|
res.status(400).json({ success: false, error: "filePath is required" });
|
|
return;
|
|
}
|
|
|
|
const resolvedPath = validatePath(filePath);
|
|
await fs.rm(resolvedPath, { recursive: true });
|
|
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
res.status(500).json({ success: false, error: message });
|
|
}
|
|
});
|
|
|
|
// Validate and add path to allowed list
|
|
// This is the web equivalent of dialog:openDirectory
|
|
router.post("/validate-path", async (req: Request, res: Response) => {
|
|
try {
|
|
const { filePath } = req.body as { filePath: string };
|
|
|
|
if (!filePath) {
|
|
res.status(400).json({ success: false, error: "filePath is required" });
|
|
return;
|
|
}
|
|
|
|
const resolvedPath = path.resolve(filePath);
|
|
|
|
// Check if path exists
|
|
try {
|
|
const stats = await fs.stat(resolvedPath);
|
|
|
|
if (!stats.isDirectory()) {
|
|
res.status(400).json({ success: false, error: "Path is not a directory" });
|
|
return;
|
|
}
|
|
|
|
// Add to allowed paths
|
|
addAllowedPath(resolvedPath);
|
|
|
|
res.json({
|
|
success: true,
|
|
path: resolvedPath,
|
|
isAllowed: isPathAllowed(resolvedPath),
|
|
});
|
|
} catch {
|
|
res.status(400).json({ success: false, error: "Path does not exist" });
|
|
}
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
res.status(500).json({ success: false, error: message });
|
|
}
|
|
});
|
|
|
|
// Save image to .automaker/images directory
|
|
router.post("/save-image", async (req: Request, res: Response) => {
|
|
try {
|
|
const { data, filename, mimeType, projectPath } = req.body as {
|
|
data: string;
|
|
filename: string;
|
|
mimeType: string;
|
|
projectPath: string;
|
|
};
|
|
|
|
if (!data || !filename || !projectPath) {
|
|
res.status(400).json({
|
|
success: false,
|
|
error: "data, filename, and projectPath are required",
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Create .automaker/images directory if it doesn't exist
|
|
const imagesDir = path.join(projectPath, ".automaker", "images");
|
|
await fs.mkdir(imagesDir, { recursive: true });
|
|
|
|
// Decode base64 data (remove data URL prefix if present)
|
|
const base64Data = data.replace(/^data:image\/\w+;base64,/, "");
|
|
const buffer = Buffer.from(base64Data, "base64");
|
|
|
|
// Generate unique filename with timestamp
|
|
const timestamp = Date.now();
|
|
const ext = path.extname(filename) || ".png";
|
|
const baseName = path.basename(filename, ext);
|
|
const uniqueFilename = `${baseName}-${timestamp}${ext}`;
|
|
const filePath = path.join(imagesDir, uniqueFilename);
|
|
|
|
// Write file
|
|
await fs.writeFile(filePath, buffer);
|
|
|
|
// Add project path to allowed paths if not already
|
|
addAllowedPath(projectPath);
|
|
|
|
res.json({ success: true, path: filePath });
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
res.status(500).json({ success: false, error: message });
|
|
}
|
|
});
|
|
|
|
// Serve image files
|
|
router.get("/image", async (req: Request, res: Response) => {
|
|
try {
|
|
const { path: imagePath, projectPath } = req.query as {
|
|
path?: string;
|
|
projectPath?: string;
|
|
};
|
|
|
|
if (!imagePath) {
|
|
res.status(400).json({ success: false, error: "path is required" });
|
|
return;
|
|
}
|
|
|
|
// Resolve full path
|
|
const fullPath = path.isAbsolute(imagePath)
|
|
? imagePath
|
|
: projectPath
|
|
? path.join(projectPath, imagePath)
|
|
: imagePath;
|
|
|
|
// Check if file exists
|
|
try {
|
|
await fs.access(fullPath);
|
|
} catch {
|
|
res.status(404).json({ success: false, error: "Image not found" });
|
|
return;
|
|
}
|
|
|
|
// Read the file
|
|
const buffer = await fs.readFile(fullPath);
|
|
|
|
// Determine MIME type from extension
|
|
const ext = path.extname(fullPath).toLowerCase();
|
|
const mimeTypes: Record<string, string> = {
|
|
".png": "image/png",
|
|
".jpg": "image/jpeg",
|
|
".jpeg": "image/jpeg",
|
|
".gif": "image/gif",
|
|
".webp": "image/webp",
|
|
".svg": "image/svg+xml",
|
|
".bmp": "image/bmp",
|
|
};
|
|
|
|
res.setHeader("Content-Type", mimeTypes[ext] || "application/octet-stream");
|
|
res.setHeader("Cache-Control", "public, max-age=3600");
|
|
res.send(buffer);
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
res.status(500).json({ success: false, error: message });
|
|
}
|
|
});
|
|
|
|
return router;
|
|
}
|