02ca134743
Backend: editor service for directory tree reading and file CRUD, editor routes at /api/editor with path traversal protection. Frontend: FileExplorer tree component with expand/collapse directories, IDELayout with sidebar + header + outlet, wired into App routing. Editor now receives state as props from App for cross-component file loading.
85 lines
2.2 KiB
TypeScript
85 lines
2.2 KiB
TypeScript
import fs from "fs/promises";
|
|
import path from "path";
|
|
|
|
const WORKSPACE_PATH = process.env.WORKSPACE_PATH || "/workspace";
|
|
|
|
export interface FileNode {
|
|
name: string;
|
|
path: string;
|
|
type: "file" | "directory";
|
|
children?: FileNode[];
|
|
}
|
|
|
|
const IGNORED_DIRS = new Set(["node_modules", ".git", ".nasher"]);
|
|
|
|
export async function getDirectoryTree(
|
|
repo: string,
|
|
subPath: string = "",
|
|
): Promise<FileNode[]> {
|
|
const fullPath = resolveRepoPath(repo, subPath);
|
|
const entries = await fs.readdir(fullPath, { withFileTypes: true });
|
|
const nodes: FileNode[] = [];
|
|
|
|
const sorted = entries.sort((a, b) => {
|
|
if (a.isDirectory() && !b.isDirectory()) return -1;
|
|
if (!a.isDirectory() && b.isDirectory()) return 1;
|
|
return a.name.localeCompare(b.name);
|
|
});
|
|
|
|
for (const entry of sorted) {
|
|
if (entry.name.startsWith(".") && entry.name !== ".gitignore") continue;
|
|
|
|
const relativePath = subPath ? `${subPath}/${entry.name}` : entry.name;
|
|
|
|
if (entry.isDirectory()) {
|
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
const children = await getDirectoryTree(repo, relativePath);
|
|
nodes.push({
|
|
name: entry.name,
|
|
path: relativePath,
|
|
type: "directory",
|
|
children,
|
|
});
|
|
} else {
|
|
nodes.push({ name: entry.name, path: relativePath, type: "file" });
|
|
}
|
|
}
|
|
|
|
return nodes;
|
|
}
|
|
|
|
export async function readFile(
|
|
repo: string,
|
|
filePath: string,
|
|
): Promise<string> {
|
|
const fullPath = resolveRepoPath(repo, filePath);
|
|
return fs.readFile(fullPath, "utf-8");
|
|
}
|
|
|
|
export async function writeFile(
|
|
repo: string,
|
|
filePath: string,
|
|
content: string,
|
|
): Promise<void> {
|
|
const fullPath = resolveRepoPath(repo, filePath);
|
|
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
await fs.writeFile(fullPath, content, "utf-8");
|
|
}
|
|
|
|
export async function deleteFile(
|
|
repo: string,
|
|
filePath: string,
|
|
): Promise<void> {
|
|
const fullPath = resolveRepoPath(repo, filePath);
|
|
await fs.unlink(fullPath);
|
|
}
|
|
|
|
function resolveRepoPath(repo: string, filePath: string): string {
|
|
const repoRoot = path.resolve(path.join(WORKSPACE_PATH, "repos", repo));
|
|
const resolved = path.resolve(repoRoot, filePath);
|
|
if (!resolved.startsWith(repoRoot)) {
|
|
throw new Error("Path traversal detected");
|
|
}
|
|
return resolved;
|
|
}
|