Showing the changes to apps/server/src/workspace/Layers/WorkspaceFileSystem.ts (1 of 62 files). Show all
Unified · Split
| @@ -1,3 +1,6 @@ | ||
| 1 | +// @effect-diagnostics nodeBuiltinImport:off | |
| 2 | +import fsPromises from "node:fs/promises"; | |
| 3 | + | |
| 1 | 4 | import * as Effect from "effect/Effect"; |
| 2 | 5 | import * as FileSystem from "effect/FileSystem"; |
| 3 | 6 | import * as Layer from "effect/Layer"; |
| @@ -11,12 +14,74 @@ import { · 7 unmodified lines | ||
| 11 | 14 | import { WorkspaceEntries } from "../Services/WorkspaceEntries.ts"; |
| 12 | 15 | import { WorkspacePaths } from "../Services/WorkspacePaths.ts"; |
| 13 | 16 | |
| 17 | +const PROJECT_READ_FILE_MAX_BYTES = 1024 * 1024; | |
| 18 | + | |
| 14 | 19 | export const makeWorkspaceFileSystem = Effect.gen(function* () { |
| 15 | 20 | const fileSystem = yield* FileSystem.FileSystem; |
| 16 | 21 | const path = yield* Path.Path; |
| 17 | 22 | const workspacePaths = yield* WorkspacePaths; |
| 18 | 23 | const workspaceEntries = yield* WorkspaceEntries; |
| 19 | 24 | |
| 25 | + const readFile: WorkspaceFileSystemShape["readFile"] = Effect.fn("WorkspaceFileSystem.readFile")( | |
| 26 | + function* (input) { | |
| 27 | + const target = yield* workspacePaths.resolveRelativePathWithinRoot({ | |
| 28 | + workspaceRoot: input.cwd, | |
| 29 | + relativePath: input.relativePath, | |
| 30 | + }); | |
| 31 | + | |
| 32 | + const result = yield* Effect.tryPromise({ | |
| 33 | + try: async () => { | |
| 34 | + const [realWorkspaceRoot, realTargetPath] = await Promise.all([ | |
| 35 | + fsPromises.realpath(input.cwd), | |
| 36 | + fsPromises.realpath(target.absolutePath), | |
| 37 | + ]); | |
| 38 | + const relativeRealPath = path.relative(realWorkspaceRoot, realTargetPath); | |
| 39 | + if ( | |
| 40 | + relativeRealPath.startsWith(`..${path.sep}`) || | |
| 41 | + relativeRealPath === ".." || | |
| 42 | + path.isAbsolute(relativeRealPath) | |
| 43 | + ) { | |
| 44 | + throw new Error("Workspace file path resolves outside the project root."); | |
| 45 | + } | |
| 46 | + | |
| 47 | + const handle = await fsPromises.open(realTargetPath, "r"); | |
| 48 | + try { | |
| 49 | + const stat = await handle.stat(); | |
| 50 | + if (!stat.isFile()) { | |
| 51 | + throw new Error("Workspace path is not a file."); | |
| 52 | + } | |
| 53 | + const bytesToRead = Math.min(stat.size, PROJECT_READ_FILE_MAX_BYTES); | |
| 54 | + const buffer = Buffer.alloc(bytesToRead); | |
| 55 | + const { bytesRead } = await handle.read(buffer, 0, bytesToRead, 0); | |
| 56 | + const fileBytes = buffer.subarray(0, bytesRead); | |
| 57 | + if (fileBytes.includes(0)) { | |
| 58 | + throw new Error("Binary files cannot be previewed as text."); | |
| 59 | + } | |
| 60 | + const contents = new TextDecoder("utf-8").decode(fileBytes); | |
| 61 | + return { | |
| 62 | + relativePath: target.relativePath, | |
| 63 | + contents, | |
| 64 | + byteLength: stat.size, | |
| 65 | + truncated: stat.size > PROJECT_READ_FILE_MAX_BYTES, | |
| 66 | + }; | |
| 67 | + } finally { | |
| 68 | + await handle.close(); | |
| 69 | + } | |
| 70 | + }, | |
| 71 | + catch: (cause) => | |
| 72 | + new WorkspaceFileSystemError({ | |
| 73 | + cwd: input.cwd, | |
| 74 | + relativePath: input.relativePath, | |
| 75 | + operation: "workspaceFileSystem.readFile", | |
| 76 | + detail: cause instanceof Error ? cause.message : String(cause), | |
| 77 | + cause, | |
| 78 | + }), | |
| 79 | + }); | |
| 80 | + | |
| 81 | + return result; | |
| 82 | + }, | |
| 83 | + ); | |
| 84 | + | |
| 20 | 85 | const writeFile: WorkspaceFileSystemShape["writeFile"] = Effect.fn( |
| 21 | 86 | "WorkspaceFileSystem.writeFile", |
| 22 | 87 | )(function* (input) { |
| @@ -52,7 +117,7 @@ export const makeWorkspaceFileSystem = Effect.gen(function* () { · 29 unmodified lines | ||
| 52 | 117 | yield* workspaceEntries.invalidate(input.cwd); |
| 53 | 118 | return { relativePath: target.relativePath }; |
| 54 | 119 | }); |
| 55 | − return { writeFile } satisfies WorkspaceFileSystemShape; | |
| 120 | + return { readFile, writeFile } satisfies WorkspaceFileSystemShape; | |
| 56 | 121 | }); |
| 57 | 122 | |
| 58 | 123 | export const WorkspaceFileSystemLive = Layer.effect(WorkspaceFileSystem, makeWorkspaceFileSystem); |