Archived
Handle missing selected workspace files
This commit is contained in:
@@ -50,13 +50,26 @@ export class FileExplorerController {
|
|||||||
if (project === undefined || workspace === undefined) return;
|
if (project === undefined || workspace === undefined) return;
|
||||||
this.setState({ selectedFilePath: path, selectedFileContent: undefined });
|
this.setState({ selectedFilePath: path, selectedFileContent: undefined });
|
||||||
try {
|
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) {
|
} 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) });
|
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> {
|
function omitKey<T>(record: Record<string, T>, keyToOmit: string): Record<string, T> {
|
||||||
return Object.fromEntries(Object.entries(record).filter(([key]) => key !== keyToOmit));
|
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, undefined)).rejects.toThrow("path query parameter is required");
|
||||||
await expect(readWorkspaceFile(root, "dir")).rejects.toThrow("Path is not a file");
|
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, "../secret.txt")).rejects.toThrow("Path traversal is not allowed");
|
||||||
await expect(readWorkspaceFile(root, "/etc/passwd")).rejects.toThrow("Absolute paths are 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 writeFile(join(root, "file.txt"), "content");
|
||||||
|
|
||||||
await expect(listWorkspaceTree(root, "file.txt")).rejects.toThrow("Path is not a directory");
|
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, "../outside")).rejects.toThrow("Path traversal is not allowed");
|
||||||
await expect(listWorkspaceTree(root, "/tmp")).rejects.toThrow("Absolute paths are not allowed");
|
await expect(listWorkspaceTree(root, "/tmp")).rejects.toThrow("Absolute paths are not allowed");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ export async function resolveInsideWorkspace(rootPath: string, relativePath: str
|
|||||||
const requested = normalizeRelativePath(relativePath);
|
const requested = normalizeRelativePath(relativePath);
|
||||||
const root = await realpath(rootPath);
|
const root = await realpath(rootPath);
|
||||||
const joined = join(root, requested);
|
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);
|
ensureInside(root, target);
|
||||||
return { root, target, relativePath: requested };
|
return { root, target, relativePath: requested };
|
||||||
}
|
}
|
||||||
@@ -27,6 +30,10 @@ export function normalizeRelativePath(input: string | undefined): string {
|
|||||||
return parts.join("/");
|
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 {
|
function ensureInside(root: string, target: string): void {
|
||||||
const rel = relative(root, target);
|
const rel = relative(root, target);
|
||||||
if (rel === "") return;
|
if (rel === "") return;
|
||||||
|
|||||||
Reference in New Issue
Block a user