-
Notifications
You must be signed in to change notification settings - Fork 962
File tree because we're just that cool #1112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e1bd7a7
98a9a08
31cf384
8309a1e
362ed77
8d5312c
07e41f8
48ba3d1
916f719
6ce1c05
7546973
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,286 @@ | ||
| import fs from "node:fs/promises"; | ||
| import path from "node:path"; | ||
| import { shell } from "electron"; | ||
| import type { DirectoryEntry } from "shared/file-tree-types"; | ||
| import { z } from "zod"; | ||
| import { publicProcedure, router } from "../.."; | ||
|
|
||
| export const createFilesystemRouter = () => { | ||
| return router({ | ||
| readDirectory: publicProcedure | ||
| .input( | ||
| z.object({ | ||
| dirPath: z.string(), | ||
| rootPath: z.string(), | ||
| includeHidden: z.boolean().default(false), | ||
| }), | ||
| ) | ||
| .query(async ({ input }): Promise<DirectoryEntry[]> => { | ||
| const { dirPath, rootPath, includeHidden } = input; | ||
|
|
||
| try { | ||
| const entries = await fs.readdir(dirPath, { withFileTypes: true }); | ||
|
|
||
| return entries | ||
| .filter((entry) => includeHidden || !entry.name.startsWith(".")) | ||
| .map((entry) => { | ||
| const fullPath = path.join(dirPath, entry.name); | ||
| const relativePath = path.relative(rootPath, fullPath); | ||
| return { | ||
| id: relativePath, | ||
| name: entry.name, | ||
| path: fullPath, | ||
| relativePath, | ||
| isDirectory: entry.isDirectory(), | ||
| }; | ||
| }) | ||
| .sort((a, b) => { | ||
| if (a.isDirectory !== b.isDirectory) { | ||
| return a.isDirectory ? -1 : 1; | ||
| } | ||
| return a.name.localeCompare(b.name); | ||
| }); | ||
| } catch (error) { | ||
| console.error("[filesystem/readDirectory] Failed:", { | ||
| dirPath, | ||
| error, | ||
| }); | ||
| return []; | ||
| } | ||
|
Comment on lines
+18
to
+49
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, verify the file exists and check its content
ls -la apps/desktop/src/lib/trpc/routers/filesystem/index.tsRepository: superset-sh/superset Length of output: 166 🏁 Script executed: # Check the full context of the readDirectory function
head -60 apps/desktop/src/lib/trpc/routers/filesystem/index.tsRepository: superset-sh/superset Length of output: 1661 🏁 Script executed: # Search for other error handling patterns in tRPC routes to ensure consistency
rg "throw new TRPCError" apps/desktop/src/lib/trpc/ -A 2 -B 2Repository: superset-sh/superset Length of output: 5451 🏁 Script executed: # Check if there are other filesystem operations that handle permissions
cat -n apps/desktop/src/lib/trpc/routers/filesystem/index.ts | tail -100Repository: superset-sh/superset Length of output: 3226 🏁 Script executed: # Look for error code handling patterns in the codebase
rg "error.code|error instanceof" apps/desktop/src/lib/trpc/routers/ -B 2 -A 2 | head -50Repository: superset-sh/superset Length of output: 4401 Don't swallow Failing reads should surface as TRPCError instead. Use 🔧 Suggested approach+import { TRPCError } from "@trpc/server";
...
} catch (error) {
console.error("[filesystem/readDirectory] Failed:", { dirPath, error });
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ if (errorMessage.includes("EACCES")) {
+ throw new TRPCError({
+ code: "FORBIDDEN",
+ message: `Permission denied: ${dirPath}`,
+ });
+ }
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: `Directory not found or inaccessible: ${dirPath}`,
+ });
- return [];
}Per coding guidelines: never swallow errors silently. Per tRPC guidelines: use 🤖 Prompt for AI Agents |
||
| }), | ||
|
|
||
| createFile: publicProcedure | ||
| .input( | ||
| z.object({ | ||
| dirPath: z.string(), | ||
| fileName: z.string(), | ||
| content: z.string().default(""), | ||
| }), | ||
| ) | ||
| .mutation(async ({ input }) => { | ||
| const filePath = path.join(input.dirPath, input.fileName); | ||
|
|
||
| try { | ||
| await fs.access(filePath); | ||
| throw new Error(`File already exists: ${input.fileName}`); | ||
| } catch (error) { | ||
| if ( | ||
| error instanceof Error && | ||
| error.message.includes("already exists") | ||
| ) { | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| await fs.writeFile(filePath, input.content, "utf-8"); | ||
| return { path: filePath }; | ||
| }), | ||
|
|
||
| createDirectory: publicProcedure | ||
| .input( | ||
| z.object({ | ||
| parentPath: z.string(), | ||
| dirName: z.string(), | ||
| }), | ||
| ) | ||
| .mutation(async ({ input }) => { | ||
| const dirPath = path.join(input.parentPath, input.dirName); | ||
|
|
||
| try { | ||
| await fs.access(dirPath); | ||
| throw new Error(`Directory already exists: ${input.dirName}`); | ||
| } catch (error) { | ||
| if ( | ||
| error instanceof Error && | ||
| error.message.includes("already exists") | ||
| ) { | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| await fs.mkdir(dirPath, { recursive: true }); | ||
| return { path: dirPath }; | ||
| }), | ||
|
|
||
| rename: publicProcedure | ||
| .input( | ||
| z.object({ | ||
| oldPath: z.string(), | ||
| newName: z.string(), | ||
| }), | ||
| ) | ||
| .mutation(async ({ input }) => { | ||
| const newPath = path.join(path.dirname(input.oldPath), input.newName); | ||
|
|
||
| try { | ||
| await fs.access(newPath); | ||
| throw new Error(`Target already exists: ${input.newName}`); | ||
| } catch (error) { | ||
| if ( | ||
| error instanceof Error && | ||
| error.message.includes("already exists") | ||
| ) { | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| await fs.rename(input.oldPath, newPath); | ||
| return { oldPath: input.oldPath, newPath }; | ||
| }), | ||
|
Comment on lines
+52
to
+129
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # Confirm file path and check for other similar patterns
head -5 apps/desktop/src/lib/trpc/routers/filesystem/index.ts
wc -l apps/desktop/src/lib/trpc/routers/filesystem/index.ts
grep -n "fs.access" apps/desktop/src/lib/trpc/routers/filesystem/index.ts
grep -n "TRPCError" apps/desktop/src/lib/trpc/routers/filesystem/index.tsRepository: superset-sh/superset Length of output: 526 🏁 Script executed: # Check what fs module is being used and if TRPCError is imported
head -30 apps/desktop/src/lib/trpc/routers/filesystem/index.tsRepository: superset-sh/superset Length of output: 1012 🏁 Script executed: # Look for similar catch patterns with message checking
rg -A 5 "error.message.includes" apps/desktop/src/lib/trpc/routers/filesystem/index.tsRepository: superset-sh/superset Length of output: 336 🏁 Script executed: # Check the other fs.access calls at lines 178, 221, 251 to see if same pattern repeats
sed -n '170,230p' apps/desktop/src/lib/trpc/routers/filesystem/index.ts | cat -n
sed -n '245,260p' apps/desktop/src/lib/trpc/routers/filesystem/index.ts | cat -nRepository: superset-sh/superset Length of output: 2646 Fix error handling in fs.access() calls to prevent masking permission and I/O errors. The Update all three procedures to:
Example fix+import { TRPCError } from "@trpc/server";
...
try {
await fs.access(filePath);
- throw new Error(`File already exists: ${input.fileName}`);
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: `File already exists: ${input.fileName}`,
+ });
} catch (error) {
- if (error instanceof Error && error.message.includes("already exists")) {
- throw error;
- }
+ if (error instanceof TRPCError) throw error;
+ const err = error as NodeJS.ErrnoException;
+ if (err?.code !== "ENOENT") {
+ console.error("[filesystem/createFile] access failed:", { filePath, error });
+ throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
+ }
}🤖 Prompt for AI Agents |
||
|
|
||
| delete: publicProcedure | ||
| .input( | ||
| z.object({ | ||
| paths: z.array(z.string()), | ||
| permanent: z.boolean().default(false), | ||
| }), | ||
| ) | ||
| .mutation(async ({ input }) => { | ||
| const deleted: string[] = []; | ||
| const errors: { path: string; error: string }[] = []; | ||
|
|
||
| for (const filePath of input.paths) { | ||
| try { | ||
| if (input.permanent) { | ||
| await fs.rm(filePath, { recursive: true, force: true }); | ||
| } else { | ||
| await shell.trashItem(filePath); | ||
| } | ||
| deleted.push(filePath); | ||
| } catch (error) { | ||
| errors.push({ | ||
| path: filePath, | ||
| error: error instanceof Error ? error.message : String(error), | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| return { deleted, errors }; | ||
| }), | ||
|
|
||
| move: publicProcedure | ||
| .input( | ||
| z.object({ | ||
| sourcePaths: z.array(z.string()), | ||
| destinationDir: z.string(), | ||
| }), | ||
| ) | ||
| .mutation(async ({ input }) => { | ||
| const moved: { from: string; to: string }[] = []; | ||
| const errors: { path: string; error: string }[] = []; | ||
|
|
||
| for (const sourcePath of input.sourcePaths) { | ||
| try { | ||
| const fileName = path.basename(sourcePath); | ||
| const destPath = path.join(input.destinationDir, fileName); | ||
|
|
||
| try { | ||
| await fs.access(destPath); | ||
| throw new Error(`Target already exists: ${fileName}`); | ||
| } catch (accessError) { | ||
| if ( | ||
| accessError instanceof Error && | ||
| accessError.message.includes("already exists") | ||
| ) { | ||
| throw accessError; | ||
| } | ||
| } | ||
|
|
||
| await fs.rename(sourcePath, destPath); | ||
| moved.push({ from: sourcePath, to: destPath }); | ||
| } catch (error) { | ||
| errors.push({ | ||
| path: sourcePath, | ||
| error: error instanceof Error ? error.message : String(error), | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| return { moved, errors }; | ||
| }), | ||
|
|
||
| copy: publicProcedure | ||
| .input( | ||
| z.object({ | ||
| sourcePaths: z.array(z.string()), | ||
| destinationDir: z.string(), | ||
| }), | ||
| ) | ||
| .mutation(async ({ input }) => { | ||
| const copied: { from: string; to: string }[] = []; | ||
| const errors: { path: string; error: string }[] = []; | ||
|
|
||
| for (const sourcePath of input.sourcePaths) { | ||
| try { | ||
| const fileName = path.basename(sourcePath); | ||
| let destPath = path.join(input.destinationDir, fileName); | ||
|
|
||
| let counter = 1; | ||
| while (true) { | ||
| try { | ||
| await fs.access(destPath); | ||
| const ext = path.extname(fileName); | ||
| const base = path.basename(fileName, ext); | ||
| destPath = path.join( | ||
| input.destinationDir, | ||
| `${base} (${counter})${ext}`, | ||
| ); | ||
| counter++; | ||
| } catch { | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| await fs.cp(sourcePath, destPath, { recursive: true }); | ||
| copied.push({ from: sourcePath, to: destPath }); | ||
| } catch (error) { | ||
| errors.push({ | ||
| path: sourcePath, | ||
| error: error instanceof Error ? error.message : String(error), | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| return { copied, errors }; | ||
| }), | ||
|
|
||
| exists: publicProcedure | ||
| .input(z.object({ path: z.string() })) | ||
| .query(async ({ input }) => { | ||
| try { | ||
| await fs.access(input.path); | ||
| const stats = await fs.stat(input.path); | ||
| return { | ||
| exists: true, | ||
| isDirectory: stats.isDirectory(), | ||
| isFile: stats.isFile(), | ||
| }; | ||
| } catch { | ||
| return { exists: false, isDirectory: false, isFile: false }; | ||
| } | ||
| }), | ||
|
|
||
| stat: publicProcedure | ||
| .input(z.object({ path: z.string() })) | ||
| .query(async ({ input }) => { | ||
| try { | ||
| const stats = await fs.stat(input.path); | ||
| return { | ||
| size: stats.size, | ||
| isDirectory: stats.isDirectory(), | ||
| isFile: stats.isFile(), | ||
| isSymbolicLink: stats.isSymbolicLink(), | ||
| createdAt: stats.birthtime.toISOString(), | ||
| modifiedAt: stats.mtime.toISOString(), | ||
| accessedAt: stats.atime.toISOString(), | ||
| }; | ||
| } catch (error) { | ||
| console.error("[filesystem/stat] Failed:", { | ||
| path: input.path, | ||
| error, | ||
| }); | ||
| return null; | ||
| } | ||
|
Comment on lines
+247
to
+283
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cat -n apps/desktop/src/lib/trpc/routers/filesystem/index.ts | sed -n '240,290p'Repository: superset-sh/superset Length of output: 1464 🏁 Script executed: head -40 apps/desktop/src/lib/trpc/routers/filesystem/index.tsRepository: superset-sh/superset Length of output: 1263 🏁 Script executed: grep -n "TRPCError" apps/desktop/src/lib/trpc/routers/filesystem/index.tsRepository: superset-sh/superset Length of output: 46 🏁 Script executed: find apps/desktop/src/lib/trpc/routers -name "*.ts" -o -name "*.tsx" | head -5Repository: superset-sh/superset Length of output: 345 🏁 Script executed: grep -l "TRPCError" apps/desktop/src/lib/trpc/routers/*.ts apps/desktop/src/lib/trpc/routers/*/*.ts 2>/dev/null | head -3Repository: superset-sh/superset Length of output: 222 🏁 Script executed: head -20 apps/desktop/src/lib/trpc/routers/external/index.tsRepository: superset-sh/superset Length of output: 561 🏁 Script executed: grep -A 5 "throw new TRPCError" apps/desktop/src/lib/trpc/routers/external/index.ts | head -20Repository: superset-sh/superset Length of output: 172 🏁 Script executed: grep -B 5 -A 3 "NOT_FOUND\|NOT_IMPLEMENTED\|FORBIDDEN" apps/desktop/src/lib/trpc/routers/*/index.ts apps/desktop/src/lib/trpc/routers/*/*/index.ts 2>/dev/null | head -40Repository: superset-sh/superset Length of output: 1508 🏁 Script executed: wc -l apps/desktop/src/lib/trpc/routers/filesystem/index.tsRepository: superset-sh/superset Length of output: 121 🏁 Script executed: grep -B 3 -A 8 "catch" apps/desktop/src/lib/trpc/routers/filesystem/index.tsRepository: superset-sh/superset Length of output: 2902 🏁 Script executed: grep -n "\.query\|\.mutation" apps/desktop/src/lib/trpc/routers/filesystem/index.ts | head -20Repository: superset-sh/superset Length of output: 438 Avoid silently swallowing filesystem errors in The For 🔧 Suggested pattern+import { TRPCError } from "@trpc/server";
...
} catch (error) {
- return { exists: false, isDirectory: false, isFile: false };
+ if ((error as NodeJS.ErrnoException)?.code === "ENOENT") {
+ return { exists: false, isDirectory: false, isFile: false };
+ }
+ console.error("[filesystem/exists] Failed:", { path: input.path, error });
+ throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
}} catch (error) {
console.error("[filesystem/stat] Failed:", {
path: input.path,
error,
});
- return null;
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: `Path not found: ${input.path}`,
+ });
}🤖 Prompt for AI Agents |
||
| }), | ||
| }); | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: superset-sh/superset
Length of output: 121
🏁 Script executed:
Repository: superset-sh/superset
Length of output: 9179
🏁 Script executed:
Repository: superset-sh/superset
Length of output: 46
🏁 Script executed:
# Look for imports and path-related utilities in the filesystem directory ls -la apps/desktop/src/lib/trpc/routers/filesystem/Repository: superset-sh/superset
Length of output: 235
🏁 Script executed:
Repository: superset-sh/superset
Length of output: 127
🏁 Script executed:
Repository: superset-sh/superset
Length of output: 8337
🏁 Script executed:
Repository: superset-sh/superset
Length of output: 18362
🏁 Script executed:
Repository: superset-sh/superset
Length of output: 46
🏁 Script executed:
Repository: superset-sh/superset
Length of output: 3624
🏁 Script executed:
Repository: superset-sh/superset
Length of output: 46
🏁 Script executed:
Repository: superset-sh/superset
Length of output: 46
🏁 Script executed:
Repository: superset-sh/superset
Length of output: 6563
🏁 Script executed:
Repository: superset-sh/superset
Length of output: 1840
🏁 Script executed:
Repository: superset-sh/superset
Length of output: 1642
Implement path validation and proper TRPC error handling throughout.
These procedures accept raw filesystem paths without boundary checks. A malicious renderer could traverse outside the allowed root using
../sequences or symlinks. Additionally, the entire router lacksTRPCErrorusage and silently swallows errors:readDirectory(line 48),exists(line 259),stat(line 282) return empty/null values on error instead of throwingcreateFile,createDirectory,renamethrow genericErrorinstead ofTRPCErrordelete,move,copycollect errors in arrays without throwingUse the existing
secureFsutilities fromapps/desktop/src/lib/trpc/routers/changes/security/secure-fs.tswhich provideassertRegisteredWorktree()andassertRealpathInWorktree()to validate paths against worktree boundaries. ThrowTRPCErrorwith appropriate codes:NOT_FOUNDfor missing files,BAD_REQUESTfor invalid input (e.g., absolute paths),INTERNAL_SERVER_ERRORfor unexpected failures.🤖 Prompt for AI Agents