Handle missing selected workspace files

This commit is contained in:
Federico Jaramillo Martinez
2026-05-11 10:41:33 +02:00
parent 1ae43998e7
commit 045f4e247a
4 changed files with 24 additions and 2 deletions
@@ -50,13 +50,26 @@ export class FileExplorerController {
if (project === undefined || workspace === undefined) return;
this.setState({ selectedFilePath: path, selectedFileContent: undefined });
try {
this.setState({ selectedFileContent: await api.workspaceFile(project.id, workspace.id, path), error: "" });
const content = await api.workspaceFile(project.id, workspace.id, path);
if (this.getState().selectedFilePath === path) this.setState({ selectedFileContent: content, error: "" });
} catch (error) {
if (this.getState().selectedFilePath !== path) return;
if (isUnavailableFileError(error)) {
this.setState({ selectedFilePath: undefined, selectedFileContent: undefined, error: "" });
setNamespacedQueryKey(FILES_ROUTE_NAMESPACE, "file", undefined, { replace: true });
this.updateUrl({ replace: true });
return;
}
this.setState({ error: String(error) });
}
}
}
function isUnavailableFileError(error: unknown): boolean {
const message = String(error);
return message.includes("Path does not exist") || message.includes("ENOENT") || message.includes("no such file or directory");
}
function omitKey<T>(record: Record<string, T>, keyToOmit: string): Record<string, T> {
return Object.fromEntries(Object.entries(record).filter(([key]) => key !== keyToOmit));
}
@@ -42,6 +42,7 @@ describe("readWorkspaceFile", () => {
await expect(readWorkspaceFile(root, undefined)).rejects.toThrow("path query parameter is required");
await expect(readWorkspaceFile(root, "dir")).rejects.toThrow("Path is not a file");
await expect(readWorkspaceFile(root, "missing.txt")).rejects.toThrow("Path does not exist");
await expect(readWorkspaceFile(root, "../secret.txt")).rejects.toThrow("Path traversal is not allowed");
await expect(readWorkspaceFile(root, "/etc/passwd")).rejects.toThrow("Absolute paths are not allowed");
});
@@ -58,6 +58,7 @@ describe("listWorkspaceTree", () => {
await writeFile(join(root, "file.txt"), "content");
await expect(listWorkspaceTree(root, "file.txt")).rejects.toThrow("Path is not a directory");
await expect(listWorkspaceTree(root, "missing-dir")).rejects.toThrow("Path does not exist");
await expect(listWorkspaceTree(root, "../outside")).rejects.toThrow("Path traversal is not allowed");
await expect(listWorkspaceTree(root, "/tmp")).rejects.toThrow("Absolute paths are not allowed");
});
+8 -1
View File
@@ -5,7 +5,10 @@ export async function resolveInsideWorkspace(rootPath: string, relativePath: str
const requested = normalizeRelativePath(relativePath);
const root = await realpath(rootPath);
const joined = join(root, requested);
const target = await realpath(joined);
const target = await realpath(joined).catch((error: unknown) => {
if (isNodeErrorWithCode(error, "ENOENT")) throw new Error("Path does not exist");
throw error;
});
ensureInside(root, target);
return { root, target, relativePath: requested };
}
@@ -27,6 +30,10 @@ export function normalizeRelativePath(input: string | undefined): string {
return parts.join("/");
}
function isNodeErrorWithCode(error: unknown, code: string): error is NodeJS.ErrnoException {
return typeof error === "object" && error !== null && "code" in error && error.code === code;
}
function ensureInside(root: string, target: string): void {
const rel = relative(root, target);
if (rel === "") return;