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
2import fsPromises from "node:fs/promises";
3
4import * as Effect from "effect/Effect";1import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";2import * as FileSystem from "effect/FileSystem";
6import * as Layer from "effect/Layer";3import * as Layer from "effect/Layer";
@@ -16,6 +13,25 @@ import { WorkspacePaths } from "../Services/WorkspacePaths.ts"; · 9 unmodified lines
16 13
17const PROJECT_READ_FILE_MAX_BYTES = 1024 * 1024;14const PROJECT_READ_FILE_MAX_BYTES = 1024 * 1024;
18 15
16function 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
31function causeMessage(cause: { readonly message: string }) {
32 return cause.message;
33}
34
19export const makeWorkspaceFileSystem = Effect.gen(function* () {35export const makeWorkspaceFileSystem = Effect.gen(function* () {
20 const fileSystem = yield* FileSystem.FileSystem;36 const fileSystem = yield* FileSystem.FileSystem;
21 const path = yield* Path.Path;37 const path = yield* Path.Path;
@@ -29,54 +45,92 @@ export const makeWorkspaceFileSystem = Effect.gen(function* () { · 7 unmodified lines
29 relativePath: input.relativePath,45 relativePath: input.relativePath,
30 });46 });
31 47
32 const result = yield* Effect.tryPromise({48 const [realWorkspaceRoot, realTargetPath] = yield* Effect.all([
33 try: async () => {49 fileSystem.realPath(input.cwd),
34 const [realWorkspaceRoot, realTargetPath] = await Promise.all([50 fileSystem.realPath(target.absolutePath),
35 fsPromises.realpath(input.cwd),51 ]).pipe(
36 fsPromises.realpath(target.absolutePath),52 Effect.mapError((cause) =>
37 ]);53 makeReadFileError({
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,54 cwd: input.cwd,
74 relativePath: input.relativePath,55 relativePath: input.relativePath,
75 operation: "workspaceFileSystem.readFile",56 detail: causeMessage(cause),
76 detail: cause instanceof Error ? cause.message : String(cause),
77 cause,57 cause,
78 }),58 }),
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 );
80 134
81 return result;135 return result;
82 },136 },