返回源码地图

packages/ssh/fs-ssh/src/index.ts

main snapshot · da00f7f5358f · 正文引用章节 10;完整原文可核对,不声称全文件人工逐行审计

完整原文供逐行核对;页面收录不代表每行都经过人工语义审核。MIT 许可见 许可证。

1/** Filesystem provider preserving remote identities and helper-owned atomic mutations. */
2import { posix } from 'node:path'
3import { pathToFileURL } from 'node:url'
4import { FileSystem, FsError } from '@deepseek-ai/dsh-fs'
5import type { FsDirEntry, FsEditOutcome, FsEditRequest, FsErrorCode, FsInfo, FsPathInfo, FsTarget, FsVersion, FsWriteIntent, FsWriteOutcome } from '@deepseek-ai/dsh-fs'
6import type { SandboxExecutionPolicy, SandboxMode } from '@deepseek-ai/dsh-sandbox'
7import type {} from '@deepseek-ai/dsh-sandbox-policy'
8import type {} from '@deepseek-ai/dsh-ssh'
9import { RemoteOperationError } from '@deepseek-ai/dsh-ssh/protocol'
10import { editResultSchema, entriesSchema, infoSchema, pathInfoSchema, targetSchema, textStreamIdSchema, writeResultSchema } from '@deepseek-ai/dsh-ssh/schemas'
11import { z } from 'zod'
12
13const errorCodes: Record<FsErrorCode, true> = {
14 FS_NOT_FOUND: true, FS_NOT_DIRECTORY: true, FS_NOT_TEXT: true, FS_NOT_REGULAR_FILE: true,
15 FS_TOO_LARGE: true, FS_PERMISSION_DENIED: true, FS_SANDBOX_DENIED: true, FS_IO_ERROR: true,
16 FS_STALE_VERSION: true, FS_NOT_OBSERVED: true, FS_AMBIGUOUS_EDIT: true, FS_EDIT_NOT_FOUND: true, FS_ABORTED: true,
17}
18
19/** Remote filesystem paired with the SSH subprocess and sandbox providers. */
20export class SshFileSystem extends FileSystem {
21 static inject = ['ssh', 'sandboxPolicy']
22
23 override get sandboxMode(): SandboxMode { return this.ctx.sandboxPolicy.defaultMode }
24
25 override async resolve(path: string, opts?: { cwd?: string; signal?: AbortSignal }): Promise<FsTarget> {
26 return await this.call('fs.resolve', { path, cwd: opts?.cwd }, targetSchema, opts?.signal) as FsTarget
27 }
28
29 override processPath(target: FsTarget): string { return String(target.targetKey) }
30
31 override fileUrl(target: FsTarget): string {
32 return pathToFileURL(this.processPath(target)).href
33 }
34
35 override contains(parent: FsTarget, child: FsTarget): boolean {
36 const path = posix.relative(this.processPath(parent), this.processPath(child))
37 return path === '' || (!path.startsWith('../') && path !== '..' && !posix.isAbsolute(path))
38 }
39
40 override async stat(target: FsTarget, signal?: AbortSignal): Promise<FsInfo | undefined> {
41 return await this.call('fs.stat', { target }, infoSchema.nullable(), signal) as FsInfo | null ?? undefined
42 }
43
44 override async lstat(path: string, opts?: { cwd?: string }, signal?: AbortSignal): Promise<FsPathInfo | undefined> {
45 return await this.call('fs.lstat', { path, cwd: opts?.cwd }, pathInfoSchema.nullable(), signal) as FsPathInfo | null ?? undefined
46 }
47
48 override readText(target: FsTarget, signal?: AbortSignal): Promise<string> {
49 return this.call('fs.readText', { target }, z.string(), signal)
50 }
51
52 override async streamText(target: FsTarget, signal?: AbortSignal): Promise<AsyncIterable<string>> {
53 const id = await this.call('fs.stream', { target }, textStreamIdSchema, signal)
54 const call = this.call.bind(this)
55 return (async function* () {
56 let ended = false
57 try {
58 while (!ended) {
59 signal?.throwIfAborted()
60 const next = await call('fs.next', { id }, z.object({ done: z.boolean(), value: z.string() }).strict(), signal)
61 ended = next.done
62 if (next.value.length > 0) yield next.value
63 }
64 } finally {
65 if (!ended) await call('fs.streamClose', { id }, z.null()).catch(() => {})
66 }
67 })()
68 }
69
70 override async readBytes(target: FsTarget, signal: AbortSignal | undefined, maxBytes: number): Promise<Uint8Array> {
71 return Buffer.from(await this.call('fs.readBytes', { target, maxBytes }, z.base64(), signal), 'base64')
72 }
73
74 override async readByteRange(target: FsTarget, range: { offset: number; length: number }, signal?: AbortSignal): Promise<Uint8Array> {
75 return Buffer.from(await this.call('fs.readRange', { target, ...range }, z.base64(), signal), 'base64')
76 }
77
78 override async listDir(target: FsTarget, signal?: AbortSignal): Promise<FsDirEntry[]> {
79 return await this.call('fs.list', { target }, entriesSchema, signal) as FsDirEntry[]
80 }
81
82 override async writeText(
83 target: FsTarget, content: string, expected?: FsWriteIntent, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy,
84 ): Promise<FsWriteOutcome> {
85 const policy = sandboxPolicy ?? this.ctx.sandboxPolicy.resolve()
86 return await this.call('fs.write', { target, content, expected, policy }, writeResultSchema, signal) as FsWriteOutcome
87 }
88
89 override async editText(
90 target: FsTarget, edit: FsEditRequest, expected?: { version: FsVersion },
91 signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy,
92 ): Promise<FsEditOutcome> {
93 const policy = sandboxPolicy ?? this.ctx.sandboxPolicy.resolve()
94 return await this.call('fs.edit', { target, edit, expected, policy }, editResultSchema, signal) as FsEditOutcome
95 }
96
97 private async call<T>(method: string, params: unknown, schema: z.ZodType<T>, signal?: AbortSignal): Promise<T> {
98 try { return await this.ctx.ssh.request(method, params, schema, signal) } catch (error) {
99 if (error instanceof RemoteOperationError && error.code !== undefined && Object.hasOwn(errorCodes, error.code)) {
100 throw new FsError(error.message, error.code as FsErrorCode, { cause: error })
101 }
102 throw new FsError(error instanceof Error ? error.message : String(error), signal?.aborted ? 'FS_ABORTED' : 'FS_IO_ERROR', { cause: error })
103 }
104 }
105}
106
107export default SshFileSystem