History

Refactor filesystem and VCS config Effect usage

Cursor Agent committed 4 months ago

Commit cb1e1691 parent 3bdaa6e4 files · +230 −83Browse files

Showing the changes to apps/server/src/workspace/Layers/WorkspaceFileSystem.ts (1 of 4 files). Show all

Unified · Split

apps/server/src/workspace/Layers/WorkspaceFileSystem.ts+101−47
@@ -1,6 +1,3 @@
1−// @effect-diagnostics nodeBuiltinImport:off
2−import fsPromises from "node:fs/promises";
3−
41 import * as Effect from "effect/Effect";
52 import * as FileSystem from "effect/FileSystem";
63 import * as Layer from "effect/Layer";
@@ -16,6 +13,25 @@ import { WorkspacePaths } from "../Services/WorkspacePaths.ts"; · 9 unmodified lines
1613
1714 const PROJECT_READ_FILE_MAX_BYTES = 1024 * 1024;
1815
16+function makeReadFileError(input: {
17+ readonly cwd: string;
18+ readonly relativePath: string;
19+ readonly detail: string;
20+ readonly cause?: unknown;
21+}) {
22+ return new WorkspaceFileSystemError({
23+ cwd: input.cwd,
24+ relativePath: input.relativePath,
25+ operation: "workspaceFileSystem.readFile",
26+ detail: input.detail,
27+ ...(input.cause === undefined ? {} : { cause: input.cause }),
28+ });
29+}
30+
31+function causeMessage(cause: { readonly message: string }) {
32+ return cause.message;
33+}
34+
1935 export const makeWorkspaceFileSystem = Effect.gen(function* () {
2036 const fileSystem = yield* FileSystem.FileSystem;
2137 const path = yield* Path.Path;
@@ -29,54 +45,92 @@ export const makeWorkspaceFileSystem = Effect.gen(function* () { · 7 unmodified lines
2945 relativePath: input.relativePath,
3046 });
3147
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({
48+ const [realWorkspaceRoot, realTargetPath] = yield* Effect.all([
49+ fileSystem.realPath(input.cwd),
50+ fileSystem.realPath(target.absolutePath),
51+ ]).pipe(
52+ Effect.mapError((cause) =>
53+ makeReadFileError({
7354 cwd: input.cwd,
7455 relativePath: input.relativePath,
75− operation: "workspaceFileSystem.readFile",
76− detail: cause instanceof Error ? cause.message : String(cause),
56+ detail: causeMessage(cause),
7757 cause,
7858 }),
79− });
59+ ),
60+ );
61+
62+ const relativeRealPath = path.relative(realWorkspaceRoot, realTargetPath);
63+ if (
64+ relativeRealPath.startsWith(`..${path.sep}`) ||
65+ relativeRealPath === ".." ||
66+ path.isAbsolute(relativeRealPath)
67+ ) {
68+ return yield* makeReadFileError({
69+ cwd: input.cwd,
70+ relativePath: input.relativePath,
71+ detail: "Workspace file path resolves outside the project root.",
72+ });
73+ }
74+
75+ const result = yield* Effect.scoped(
76+ Effect.gen(function* () {
77+ const handle = yield* fileSystem.open(realTargetPath, { flag: "r" }).pipe(
78+ Effect.mapError((cause) =>
79+ makeReadFileError({
80+ cwd: input.cwd,
81+ relativePath: input.relativePath,
82+ detail: causeMessage(cause),
83+ cause,
84+ }),
85+ ),
86+ );
87+ const stat = yield* handle.stat.pipe(
88+ Effect.mapError((cause) =>
89+ makeReadFileError({
90+ cwd: input.cwd,
91+ relativePath: input.relativePath,
92+ detail: causeMessage(cause),
93+ cause,
94+ }),
95+ ),
96+ );
97+ if (stat.type !== "File") {
98+ return yield* makeReadFileError({
99+ cwd: input.cwd,
100+ relativePath: input.relativePath,
101+ detail: "Workspace path is not a file.",
102+ });
103+ }
104+ const truncated = stat.size > FileSystem.Size(PROJECT_READ_FILE_MAX_BYTES);
105+ const bytesToRead = truncated ? PROJECT_READ_FILE_MAX_BYTES : Number(stat.size);
106+ const buffer = new Uint8Array(bytesToRead);
107+ const bytesRead = yield* handle.read(buffer).pipe(
108+ Effect.mapError((cause) =>
109+ makeReadFileError({
110+ cwd: input.cwd,
111+ relativePath: input.relativePath,
112+ detail: causeMessage(cause),
113+ cause,
114+ }),
115+ ),
116+ );
117+ const fileBytes = buffer.subarray(0, Number(bytesRead));
118+ if (fileBytes.includes(0)) {
119+ return yield* makeReadFileError({
120+ cwd: input.cwd,
121+ relativePath: input.relativePath,
122+ detail: "Binary files cannot be previewed as text.",
123+ });
124+ }
125+ const contents = new TextDecoder("utf-8").decode(fileBytes);
126+ return {
127+ relativePath: target.relativePath,
128+ contents,
129+ byteLength: Number(stat.size),
130+ truncated,
131+ };
132+ }),
133+ );
80134
81135 return result;
82136 },