History

feat(files): open markdown, HTML, and PDF files outside the workspace (#9140)

Julius Marminge committed 2 months ago

Commit f46a7091 parent f2a914b26 files · +394 −89Browse files

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

Unified · Split

apps/server/src/workspace/WorkspaceFileSystem.ts+48−6
@@ -3,10 +3,13 @@ · 2 unmodified lines
3 * WorkspaceFileSystem - Effect service contract for workspace file mutations.3 * WorkspaceFileSystem - Effect service contract for workspace file mutations.
4 *4 *
5 * Owns workspace-root-relative file read/write operations and their associated5 * Owns workspace-root-relative file read/write operations and their associated
6 * safety checks and cache invalidation hooks.6 * safety checks and cache invalidation hooks. Reads also accept absolute host
7 * paths so clients can show files an agent left outside the workspace; writes
8 * never leave the root.
7 *9 *
8 * @module WorkspaceFileSystem10 * @module WorkspaceFileSystem
9 */11 */
12import * as NodeFS from "node:fs";
10import * as NodeFSP from "node:fs/promises";13import * as NodeFSP from "node:fs/promises";
11 14
12import type {15import type {
@@ -104,7 +107,10 @@ export type WorkspaceFileSystemError = typeof WorkspaceFileSystemError.Type; · 91 unmodified lines
104export class WorkspaceFileSystem extends Context.Service<107export class WorkspaceFileSystem extends Context.Service<
105 WorkspaceFileSystem,108 WorkspaceFileSystem,
106 {109 {
107 /** Read a UTF-8 text file relative to the workspace root. */110 /**
111 * Read a UTF-8 text file relative to the workspace root, or any host file by
112 * absolute path.
113 */
108 readonly readFile: (114 readonly readFile: (
109 input: ProjectReadFileInput,115 input: ProjectReadFileInput,
110 ) => Effect.Effect<116 ) => Effect.Effect<
@@ -132,9 +138,31 @@ export const make = Effect.gen(function* () { · 21 unmodified lines
132 const workspacePaths = yield* WorkspacePaths.WorkspacePaths;138 const workspacePaths = yield* WorkspacePaths.WorkspacePaths;
133 const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;139 const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;
134 140
135 const readFile: WorkspaceFileSystem["Service"]["readFile"] = Effect.fn(141 /**
136 "WorkspaceFileSystem.readFile",142 * Resolves the file a read targets. Workspace-relative paths must stay inside the
137 )(function* (input) {143 * root, symlinks included. An absolute path reads a host file in place, such as a
144 * report an agent wrote to a temp directory; it gets no root check.
145 */
146 const resolveReadTarget = Effect.fn("WorkspaceFileSystem.resolveReadTarget")(function* (
147 input: ProjectReadFileInput,
148 ) {
149 const requestedPath = input.relativePath.trim();
150 if (path.isAbsolute(requestedPath)) {
151 const realTargetPath = yield* Effect.tryPromise({
152 try: () => NodeFSP.realpath(requestedPath),
153 catch: (cause) =>
154 new WorkspaceFileSystemOperationError({
155 workspaceRoot: input.cwd,
156 relativePath: input.relativePath,
157 resolvedPath: requestedPath,
158 operationPath: requestedPath,
159 operation: "realpath-target",
160 cause,
161 }),
162 });
163 return { relativePath: requestedPath, realTargetPath };
164 }
165
138 const target = yield* workspacePaths.resolveRelativePathWithinRoot({166 const target = yield* workspacePaths.resolveRelativePathWithinRoot({
139 workspaceRoot: input.cwd,167 workspaceRoot: input.cwd,
140 relativePath: input.relativePath,168 relativePath: input.relativePath,
@@ -177,10 +205,24 @@ export const make = Effect.gen(function* () { · 36 unmodified lines
177 resolvedPath: realTargetPath,205 resolvedPath: realTargetPath,
178 });206 });
179 }207 }
208 return { relativePath: target.relativePath, realTargetPath };
209 });
210
211 const readFile: WorkspaceFileSystem["Service"]["readFile"] = Effect.fn(
212 "WorkspaceFileSystem.readFile",
213 )(function* (input) {
214 const target = yield* resolveReadTarget(input);
215 const realTargetPath = target.realTargetPath;
180 216
181 return yield* Effect.acquireUseRelease(217 return yield* Effect.acquireUseRelease(
182 Effect.tryPromise({218 Effect.tryPromise({
183 try: () => NodeFSP.open(realTargetPath, "r"),219 // Non-blocking so a FIFO cannot hang the open; the stat below rejects
220 // it. Regular files ignore the flag. Windows lacks it.
221 try: () =>
222 NodeFSP.open(
223 realTargetPath,
224 NodeFS.constants.O_RDONLY | (NodeFS.constants.O_NONBLOCK ?? 0),
225 ),
184 catch: (cause) =>226 catch: (cause) =>
185 new WorkspaceFileSystemOperationError({227 new WorkspaceFileSystemOperationError({
186 workspaceRoot: input.cwd,228 workspaceRoot: input.cwd,