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 | import * as Effect from "effect/Effect"; | 4 | import * as Effect from "effect/Effect"; |
| 2 | import * as FileSystem from "effect/FileSystem"; | 5 | import * as FileSystem from "effect/FileSystem"; |
| 3 | import * as Layer from "effect/Layer"; | 6 | import * as Layer from "effect/Layer"; |
| @@ -11,12 +14,74 @@ import { · 7 unmodified lines | |||
| 11 | import { WorkspaceEntries } from "../Services/WorkspaceEntries.ts"; | 14 | import { WorkspaceEntries } from "../Services/WorkspaceEntries.ts"; |
| 12 | import { WorkspacePaths } from "../Services/WorkspacePaths.ts"; | 15 | import { WorkspacePaths } from "../Services/WorkspacePaths.ts"; |
| 13 | 16 | ||
| 17 | const PROJECT_READ_FILE_MAX_BYTES = 1024 * 1024; | ||
| 18 | |||
| 14 | export const makeWorkspaceFileSystem = Effect.gen(function* () { | 19 | export const makeWorkspaceFileSystem = Effect.gen(function* () { |
| 15 | const fileSystem = yield* FileSystem.FileSystem; | 20 | const fileSystem = yield* FileSystem.FileSystem; |
| 16 | const path = yield* Path.Path; | 21 | const path = yield* Path.Path; |
| 17 | const workspacePaths = yield* WorkspacePaths; | 22 | const workspacePaths = yield* WorkspacePaths; |
| 18 | const workspaceEntries = yield* WorkspaceEntries; | 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 | const writeFile: WorkspaceFileSystemShape["writeFile"] = Effect.fn( | 85 | const writeFile: WorkspaceFileSystemShape["writeFile"] = Effect.fn( |
| 21 | "WorkspaceFileSystem.writeFile", | 86 | "WorkspaceFileSystem.writeFile", |
| 22 | )(function* (input) { | 87 | )(function* (input) { |
| @@ -52,7 +117,7 @@ export const makeWorkspaceFileSystem = Effect.gen(function* () { · 29 unmodified lines | |||
| 52 | yield* workspaceEntries.invalidate(input.cwd); | 117 | yield* workspaceEntries.invalidate(input.cwd); |
| 53 | return { relativePath: target.relativePath }; | 118 | return { relativePath: target.relativePath }; |
| 54 | }); | 119 | }); |
| 55 | return { writeFile } satisfies WorkspaceFileSystemShape; | 120 | return { readFile, writeFile } satisfies WorkspaceFileSystemShape; |
| 56 | }); | 121 | }); |
| 57 | 122 | ||
| 58 | export const WorkspaceFileSystemLive = Layer.effect(WorkspaceFileSystem, makeWorkspaceFileSystem); | 123 | export const WorkspaceFileSystemLive = Layer.effect(WorkspaceFileSystem, makeWorkspaceFileSystem); |