index.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582
  1. /**
  2. * E2B provider for the filesystem capability seam. Paths, contents, and
  3. * atomic staging files remain inside the shared remote sandbox.
  4. * @module @deepseek-ai/dsh-fs-e2b
  5. */
  6. import { createHash, randomUUID } from 'node:crypto'
  7. import { Buffer } from 'node:buffer'
  8. import { posix } from 'node:path'
  9. import { FileSystem, FsError, FsTargetKey, FsVersion } from '@deepseek-ai/dsh-fs'
  10. import type {
  11. FsDirEntry,
  12. FsEditOutcome,
  13. FsEditRequest,
  14. FsInfo,
  15. FsPathInfo,
  16. FsTarget,
  17. FsWriteIntent,
  18. FsWriteOutcome,
  19. } from '@deepseek-ai/dsh-fs'
  20. import {
  21. CommandExitError,
  22. e2bControlEnvs,
  23. FileNotFoundError,
  24. FileType,
  25. quoteE2BShellArg,
  26. } from '@deepseek-ai/dsh-e2b'
  27. import type { EntryInfo, Sandbox } from '@deepseek-ai/dsh-e2b'
  28. const VERSION_METADATA_KEY = 'dsh-version'
  29. const BINARY_SAMPLE_BYTES = 8192
  30. const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/
  31. function assertNotAborted(signal: AbortSignal | undefined, operation: string): void {
  32. if (signal?.aborted === true) throw new FsError(`${operation} aborted`, 'FS_ABORTED')
  33. }
  34. function normalizeLineEndings(value: string): string {
  35. return value.replaceAll('\r\n', '\n')
  36. }
  37. function detectsCrlf(value: string): boolean {
  38. const sample = value.slice(0, 4096)
  39. const crlf = sample.split('\r\n').length - 1
  40. const lf = sample.split('\n').length - 1 - crlf
  41. return crlf > lf
  42. }
  43. function restoreLineEndings(value: string, crlf: boolean): string {
  44. return crlf ? normalizeLineEndings(value).replaceAll('\n', '\r\n') : value
  45. }
  46. function decodeText(bytes: Uint8Array, displayPath: string, binarySampleBytes: number): string {
  47. if (bytes.subarray(0, binarySampleBytes).includes(0)) {
  48. throw new FsError(`cannot read "${displayPath}": binary file`, 'FS_NOT_TEXT')
  49. }
  50. try {
  51. return new TextDecoder('utf-8', { fatal: true }).decode(bytes)
  52. } catch (error: unknown) {
  53. throw new FsError(`cannot read "${displayPath}": invalid UTF-8 text`, 'FS_NOT_TEXT', { cause: error })
  54. }
  55. }
  56. function decodeCanonicalPath(encoded: string): string {
  57. if (encoded.length === 0 || !BASE64.test(encoded)) {
  58. throw new Error('fs-e2b: canonical path transport returned invalid base64')
  59. }
  60. const framed = Buffer.from(encoded, 'base64')
  61. if (framed.toString('base64') !== encoded
  62. || framed.length < 2
  63. || framed.at(-1) !== 0
  64. || framed.subarray(0, -1).includes(0)) {
  65. throw new Error('fs-e2b: canonical path transport returned invalid NUL framing')
  66. }
  67. let path: string
  68. try {
  69. path = new TextDecoder('utf-8', { fatal: true }).decode(framed.subarray(0, -1))
  70. } catch (error: unknown) {
  71. throw new Error('fs-e2b: canonical path is not valid UTF-8', { cause: error })
  72. }
  73. if (!posix.isAbsolute(path)) throw new Error('fs-e2b: canonical path is not absolute')
  74. return path
  75. }
  76. function signalOpts(signal: AbortSignal | undefined): { signal?: AbortSignal } {
  77. return signal === undefined ? {} : { signal }
  78. }
  79. function commandOpts(signal: AbortSignal | undefined): { envs: Record<string, string>; signal?: AbortSignal } {
  80. return { envs: e2bControlEnvs(), ...signalOpts(signal) }
  81. }
  82. async function openReadStream(
  83. sandbox: Sandbox,
  84. target: FsTarget,
  85. signal: AbortSignal | undefined,
  86. ): Promise<ReadableStream<Uint8Array>> {
  87. try {
  88. // The pinned SDK's stream overload lies for empty files: content-length 0
  89. // returns '' instead of a ReadableStream.
  90. const read = await sandbox.files.read(String(target.targetKey), { format: 'stream', ...signalOpts(signal) }) as
  91. ReadableStream<Uint8Array> | string
  92. return typeof read === 'string'
  93. ? new ReadableStream<Uint8Array>({ start(controller) { controller.close() } })
  94. : read
  95. } catch (error: unknown) {
  96. throw mapError(error, 'read', target.displayPath, signal)
  97. }
  98. }
  99. function entryType(entry: EntryInfo): FsInfo['type'] {
  100. switch (entry.type) {
  101. case FileType.FILE:
  102. return 'file'
  103. case FileType.DIR:
  104. return 'directory'
  105. default:
  106. return 'other'
  107. }
  108. }
  109. function entryVersion(entry: EntryInfo): ReturnType<typeof FsVersion> {
  110. const facts = JSON.stringify([
  111. entry.metadata?.[VERSION_METADATA_KEY],
  112. entry.path,
  113. entry.type,
  114. entry.size,
  115. entry.mode,
  116. entry.modifiedTime?.toISOString(),
  117. entry.symlinkTarget,
  118. ])
  119. return FsVersion(`e2b:${createHash('sha256').update(facts).digest('hex')}`)
  120. }
  121. function mapError(error: unknown, operation: string, displayPath: string, signal?: AbortSignal): FsError {
  122. if (error instanceof FsError) return error
  123. if (signal?.aborted === true || (error instanceof DOMException && error.name === 'AbortError')) {
  124. return new FsError(`${operation} aborted`, 'FS_ABORTED', { cause: error })
  125. }
  126. if (error instanceof FileNotFoundError) {
  127. return new FsError(`cannot ${operation} "${displayPath}": not found`, 'FS_NOT_FOUND', { cause: error })
  128. }
  129. if (/permission denied|operation not permitted/i.test(String(error))) {
  130. return new FsError(`cannot ${operation} "${displayPath}": permission denied`, 'FS_PERMISSION_DENIED', { cause: error })
  131. }
  132. return new FsError(`cannot ${operation} "${displayPath}": ${String(error)}`, 'FS_IO_ERROR', { cause: error })
  133. }
  134. function literalEdit(content: string, request: FsEditRequest, displayPath: string): string {
  135. const oldString = normalizeLineEndings(request.oldString)
  136. const newString = normalizeLineEndings(request.newString)
  137. if (oldString.length === 0) {
  138. throw new FsError(`cannot edit "${displayPath}": old_string must be non-empty`, 'FS_EDIT_NOT_FOUND')
  139. }
  140. let matches = 0
  141. let offset = 0
  142. while (true) {
  143. const found = content.indexOf(oldString, offset)
  144. if (found < 0) break
  145. matches += 1
  146. offset = found + oldString.length
  147. }
  148. if (matches === 0) throw new FsError(`cannot edit "${displayPath}": old_string was not found`, 'FS_EDIT_NOT_FOUND')
  149. if (!request.replaceAll && matches !== 1) {
  150. throw new FsError(`cannot edit "${displayPath}": old_string matched ${matches} times`, 'FS_AMBIGUOUS_EDIT')
  151. }
  152. return request.replaceAll ? content.split(oldString).join(newString) : content.replace(oldString, newString)
  153. }
  154. /** Remote filesystem backend sharing the sandbox owned by `ctx.e2b`. */
  155. export class E2BFileSystem extends FileSystem {
  156. static inject = ['e2b']
  157. private readonly locks = new Map<string, Promise<unknown>>()
  158. override async resolve(path: string, opts?: { cwd?: string; signal?: AbortSignal }): Promise<FsTarget> {
  159. assertNotAborted(opts?.signal, 'resolve')
  160. if (path.trim().length === 0) throw new FsError('file_path must be a non-empty string', 'FS_NOT_FOUND')
  161. const displayPath = posix.resolve(opts?.cwd ?? this.ctx.e2b.cwd, path)
  162. try {
  163. const sandbox = await this.ctx.e2b.getSandbox()
  164. const targetKey = await this.canonicalPath(sandbox, displayPath, opts?.signal)
  165. assertNotAborted(opts?.signal, 'resolve')
  166. return { targetKey: FsTargetKey(targetKey), displayPath }
  167. } catch (error: unknown) {
  168. throw mapError(error, 'resolve', displayPath, opts?.signal)
  169. }
  170. }
  171. override processPath(target: FsTarget): string {
  172. return String(target.targetKey)
  173. }
  174. override fileUrl(target: FsTarget): string {
  175. const path = this.processPath(target)
  176. if (!posix.isAbsolute(path)) throw new Error(`fs-e2b: expected an absolute process path: ${JSON.stringify(path)}`)
  177. return `file://${path.split('/').map(segment => encodeURIComponent(segment)).join('/')}`
  178. }
  179. override contains(parent: FsTarget, child: FsTarget): boolean {
  180. const relative = posix.relative(this.processPath(parent), this.processPath(child))
  181. return relative === '' || (relative !== '..' && !relative.startsWith('../') && !posix.isAbsolute(relative))
  182. }
  183. override async stat(target: FsTarget, signal?: AbortSignal): Promise<FsInfo | undefined> {
  184. assertNotAborted(signal, 'stat')
  185. const entry = await this.probe(String(target.targetKey), target.displayPath, signal)
  186. if (entry === undefined) return undefined
  187. return {
  188. version: entryVersion(entry),
  189. type: entryType(entry),
  190. ...(entry.type === FileType.FILE ? { size: entry.size } : {}),
  191. }
  192. }
  193. override async lstat(path: string, opts?: { cwd?: string }, signal?: AbortSignal): Promise<FsPathInfo | undefined> {
  194. assertNotAborted(signal, 'lstat')
  195. if (path.trim().length === 0) throw new FsError('file_path must be a non-empty string', 'FS_NOT_FOUND')
  196. const displayPath = posix.resolve(opts?.cwd ?? this.ctx.e2b.cwd, path)
  197. const entry = await this.probe(displayPath, displayPath, signal)
  198. if (entry === undefined) return undefined
  199. const type = entry.symlinkTarget !== undefined
  200. ? 'symlink' as const
  201. : entry.type === FileType.FILE
  202. ? 'file' as const
  203. : entry.type === FileType.DIR
  204. ? 'directory' as const
  205. : 'other' as const
  206. return {
  207. version: entryVersion(entry),
  208. type,
  209. ...(entry.type === FileType.FILE ? { size: entry.size } : {}),
  210. }
  211. }
  212. override async readText(target: FsTarget, signal?: AbortSignal): Promise<string> {
  213. const sandbox = await this.ctx.e2b.getSandbox()
  214. await this.requireRegular(target, signal)
  215. try {
  216. const bytes = await sandbox.files.read(String(target.targetKey), { format: 'bytes', ...signalOpts(signal) })
  217. assertNotAborted(signal, 'read')
  218. return decodeText(bytes, target.displayPath, BINARY_SAMPLE_BYTES)
  219. } catch (error: unknown) {
  220. throw mapError(error, 'read', target.displayPath, signal)
  221. }
  222. }
  223. override async readBytes(target: FsTarget, signal: AbortSignal | undefined, maxBytes: number): Promise<Uint8Array> {
  224. const sandbox = await this.ctx.e2b.getSandbox()
  225. const info = await this.requireRegular(target, signal)
  226. if (info.size !== undefined && info.size > maxBytes) {
  227. throw new FsError(`cannot read "${target.displayPath}": ${info.size} bytes exceeds the ${maxBytes}-byte limit`, 'FS_TOO_LARGE')
  228. }
  229. const stream = await openReadStream(sandbox, target, signal)
  230. const reader = stream.getReader()
  231. const chunks: Uint8Array[] = []
  232. let bytes = 0
  233. let completed = false
  234. try {
  235. while (true) {
  236. assertNotAborted(signal, 'read')
  237. const next = await reader.read()
  238. if (next.done) break
  239. // The stat preflight covers the at-rest case; this streamed bound stops
  240. // a post-stat grower without transferring past the first overflowing chunk.
  241. bytes += next.value.byteLength
  242. if (bytes > maxBytes) {
  243. throw new FsError(`cannot read "${target.displayPath}": content exceeds the ${maxBytes}-byte limit`, 'FS_TOO_LARGE')
  244. }
  245. chunks.push(next.value)
  246. }
  247. completed = true
  248. } catch (error: unknown) {
  249. throw mapError(error, 'read', target.displayPath, signal)
  250. } finally {
  251. if (!completed) {
  252. try {
  253. await reader.cancel()
  254. } catch (_streamCancellationFailure) {
  255. // The read already failed; a cancellation failure on the abandoned
  256. // remote stream adds nothing actionable for the caller.
  257. }
  258. }
  259. reader.releaseLock()
  260. }
  261. const whole = new Uint8Array(bytes)
  262. let offset = 0
  263. for (const chunk of chunks) {
  264. whole.set(chunk, offset)
  265. offset += chunk.byteLength
  266. }
  267. return whole
  268. }
  269. override async streamText(target: FsTarget, signal?: AbortSignal): Promise<AsyncIterable<string>> {
  270. const sandbox = await this.ctx.e2b.getSandbox()
  271. await this.requireRegular(target, signal)
  272. const stream = await openReadStream(sandbox, target, signal)
  273. const displayPath = target.displayPath
  274. return {
  275. async *[Symbol.asyncIterator](): AsyncGenerator<string> {
  276. const reader = stream.getReader()
  277. const decoder = new TextDecoder('utf-8', { fatal: true })
  278. let sampledBytes = 0
  279. let completed = false
  280. try {
  281. while (true) {
  282. assertNotAborted(signal, 'read')
  283. const next = await reader.read()
  284. if (next.done) break
  285. if (sampledBytes < BINARY_SAMPLE_BYTES) {
  286. const sample = next.value.subarray(0, BINARY_SAMPLE_BYTES - sampledBytes)
  287. if (sample.includes(0)) throw new FsError(`cannot read "${displayPath}": binary file`, 'FS_NOT_TEXT')
  288. sampledBytes += sample.length
  289. }
  290. let text: string
  291. try {
  292. text = decoder.decode(next.value, { stream: true })
  293. } catch (error: unknown) {
  294. throw new FsError(`cannot read "${displayPath}": invalid UTF-8 text`, 'FS_NOT_TEXT', { cause: error })
  295. }
  296. if (text.length > 0) yield text
  297. }
  298. try {
  299. decoder.decode()
  300. } catch (error: unknown) {
  301. throw new FsError(`cannot read "${displayPath}": invalid UTF-8 text`, 'FS_NOT_TEXT', { cause: error })
  302. }
  303. completed = true
  304. } catch (error: unknown) {
  305. throw mapError(error, 'read', displayPath, signal)
  306. } finally {
  307. if (!completed) {
  308. try {
  309. await reader.cancel()
  310. } catch (_streamCancellationFailure) {
  311. // The primary read outcome owns the result; cancellation is best-effort after early stop.
  312. }
  313. }
  314. reader.releaseLock()
  315. }
  316. },
  317. }
  318. }
  319. override async listDir(target: FsTarget, signal?: AbortSignal): Promise<FsDirEntry[]> {
  320. const info = await this.stat(target, signal)
  321. if (info === undefined) throw new FsError(`cannot list "${target.displayPath}": not found`, 'FS_NOT_FOUND')
  322. if (info.type !== 'directory') throw new FsError(`cannot list "${target.displayPath}": not a directory`, 'FS_NOT_DIRECTORY')
  323. try {
  324. const sandbox = await this.ctx.e2b.getSandbox()
  325. const listed = await sandbox.files.list(String(target.targetKey), { depth: 1, ...signalOpts(signal) })
  326. const entries: FsDirEntry[] = []
  327. for (const entry of listed) {
  328. const displayPath = posix.join(target.displayPath, entry.name)
  329. const canonical = entry.symlinkTarget === undefined
  330. ? entry.path
  331. : await this.canonicalPath(sandbox, entry.path, signal)
  332. const resolved = entry.symlinkTarget === undefined
  333. ? entry
  334. : await this.probe(canonical, displayPath, signal)
  335. entries.push({
  336. name: entry.name,
  337. type: resolved === undefined ? 'other' : entryType(resolved),
  338. target: { targetKey: FsTargetKey(canonical), displayPath },
  339. ...(resolved !== undefined ? { version: entryVersion(resolved) } : {}),
  340. ...(resolved?.type === FileType.FILE ? { size: resolved.size } : {}),
  341. })
  342. }
  343. return entries.sort((left, right) => left.name.localeCompare(right.name))
  344. } catch (error: unknown) {
  345. throw mapError(error, 'list', target.displayPath, signal)
  346. }
  347. }
  348. override async writeText(
  349. target: FsTarget,
  350. content: string,
  351. expected?: FsWriteIntent,
  352. signal?: AbortSignal,
  353. ): Promise<FsWriteOutcome> {
  354. return this.withLock(String(target.targetKey), async () => {
  355. const existing = await this.probe(String(target.targetKey), target.displayPath, signal)
  356. if (existing !== undefined && entryType(existing) !== 'file') {
  357. throw new FsError(`cannot write "${target.displayPath}": not a regular file`, 'FS_NOT_REGULAR_FILE')
  358. }
  359. this.checkWriteIntent(existing, expected, target)
  360. const before = existing === undefined ? null : await this.readForDiff(target, signal)
  361. const version = await this.writeAtomic(
  362. target,
  363. content,
  364. existing,
  365. expected?.kind === 'createIfAbsent',
  366. signal,
  367. )
  368. return {
  369. operation: existing === undefined ? 'create' : 'update',
  370. version,
  371. before,
  372. after: normalizeLineEndings(content),
  373. }
  374. })
  375. }
  376. override async editText(
  377. target: FsTarget,
  378. edit: FsEditRequest,
  379. expected?: { version: ReturnType<typeof FsVersion> },
  380. signal?: AbortSignal,
  381. ): Promise<FsEditOutcome> {
  382. return this.withLock(String(target.targetKey), async () => {
  383. const existing = await this.probe(String(target.targetKey), target.displayPath, signal)
  384. if (existing === undefined) {
  385. throw new FsError(`cannot edit "${target.displayPath}": file changed since it was read`, 'FS_STALE_VERSION')
  386. }
  387. if (entryType(existing) !== 'file') {
  388. throw new FsError(`cannot edit "${target.displayPath}": not a regular file`, 'FS_NOT_REGULAR_FILE')
  389. }
  390. if (expected !== undefined && entryVersion(existing) !== expected.version) {
  391. throw new FsError(`cannot edit "${target.displayPath}": file changed since it was read`, 'FS_STALE_VERSION')
  392. }
  393. const raw = await this.readForEdit(target, signal)
  394. const before = normalizeLineEndings(raw)
  395. const after = literalEdit(before, edit, target.displayPath)
  396. const storage = restoreLineEndings(after, detectsCrlf(raw))
  397. const version = await this.writeAtomic(target, storage, existing, false, signal)
  398. return { version, before, after }
  399. })
  400. }
  401. private async withLock<T>(targetKey: string, operation: () => Promise<T>): Promise<T> {
  402. const prior = this.locks.get(targetKey) ?? Promise.resolve()
  403. const run = prior.then(operation, operation)
  404. const tail = run.then(() => undefined, () => undefined)
  405. this.locks.set(targetKey, tail)
  406. try {
  407. return await run
  408. } finally {
  409. if (this.locks.get(targetKey) === tail) this.locks.delete(targetKey)
  410. }
  411. }
  412. private async canonicalPath(sandbox: Sandbox, path: string, signal?: AbortSignal): Promise<string> {
  413. try {
  414. const result = await sandbox.commands.run(
  415. `set -o pipefail; realpath -mz -- ${quoteE2BShellArg(path)} | base64 -w0`,
  416. commandOpts(signal),
  417. )
  418. return decodeCanonicalPath(result.stdout)
  419. } catch (error: unknown) {
  420. if (error instanceof CommandExitError) throw new Error(error.stderr || error.message, { cause: error })
  421. throw error
  422. }
  423. }
  424. private async probe(path: string, displayPath: string, signal?: AbortSignal): Promise<EntryInfo | undefined> {
  425. assertNotAborted(signal, 'stat')
  426. try {
  427. const sandbox = await this.ctx.e2b.getSandbox()
  428. const entry = await sandbox.files.getInfo(path, signalOpts(signal))
  429. assertNotAborted(signal, 'stat')
  430. return entry
  431. } catch (error: unknown) {
  432. if (error instanceof FileNotFoundError) return undefined
  433. throw mapError(error, 'stat', displayPath, signal)
  434. }
  435. }
  436. private async requireRegular(target: FsTarget, signal?: AbortSignal): Promise<FsInfo> {
  437. const info = await this.stat(target, signal)
  438. if (info === undefined) throw new FsError(`cannot read "${target.displayPath}": not found`, 'FS_NOT_FOUND')
  439. if (info.type !== 'file') throw new FsError(`cannot read "${target.displayPath}": not a regular file`, 'FS_NOT_REGULAR_FILE')
  440. return info
  441. }
  442. private checkWriteIntent(existing: EntryInfo | undefined, expected: FsWriteIntent | undefined, target: FsTarget): void {
  443. if (expected?.kind === 'createIfAbsent' && existing !== undefined) {
  444. throw new FsError(`cannot overwrite existing "${target.displayPath}" without reading it first`, 'FS_NOT_OBSERVED')
  445. }
  446. if (expected?.kind === 'replaceIfVersion') {
  447. if (existing === undefined || entryVersion(existing) !== expected.version) {
  448. throw new FsError(`cannot write "${target.displayPath}": file changed since it was read`, 'FS_STALE_VERSION')
  449. }
  450. }
  451. }
  452. private async readForDiff(target: FsTarget, signal?: AbortSignal): Promise<string | null> {
  453. try {
  454. const sandbox = await this.ctx.e2b.getSandbox()
  455. const bytes = await sandbox.files.read(String(target.targetKey), { format: 'bytes', ...signalOpts(signal) })
  456. assertNotAborted(signal, 'read')
  457. return normalizeLineEndings(decodeText(bytes, target.displayPath, bytes.length))
  458. } catch (error: unknown) {
  459. if (error instanceof FsError && error.code === 'FS_NOT_TEXT') return null
  460. throw mapError(error, 'read', target.displayPath, signal)
  461. }
  462. }
  463. private async readForEdit(target: FsTarget, signal?: AbortSignal): Promise<string> {
  464. try {
  465. const sandbox = await this.ctx.e2b.getSandbox()
  466. const bytes = await sandbox.files.read(String(target.targetKey), { format: 'bytes', ...signalOpts(signal) })
  467. assertNotAborted(signal, 'edit')
  468. return decodeText(bytes, target.displayPath, bytes.length)
  469. } catch (error: unknown) {
  470. throw mapError(error, 'edit', target.displayPath, signal)
  471. }
  472. }
  473. private async writeAtomic(
  474. target: FsTarget,
  475. content: string,
  476. existing: EntryInfo | undefined,
  477. createIfAbsent: boolean,
  478. signal?: AbortSignal,
  479. ): Promise<ReturnType<typeof FsVersion>> {
  480. assertNotAborted(signal, 'write')
  481. const sandbox = await this.ctx.e2b.getSandbox()
  482. const targetPath = String(target.targetKey)
  483. const versionId = randomUUID()
  484. const stagingDirectory = posix.join(posix.dirname(targetPath), `.dsh-${randomUUID()}.tmp`)
  485. const temporary = posix.join(stagingDirectory, 'content')
  486. let stagingDirectoryCreated = false
  487. try {
  488. const created = await sandbox.files.makeDir(stagingDirectory, signalOpts(signal))
  489. if (!created) throw new Error('private staging directory already exists')
  490. stagingDirectoryCreated = true
  491. await sandbox.commands.run(`chmod 700 -- ${quoteE2BShellArg(stagingDirectory)}`, commandOpts(signal))
  492. assertNotAborted(signal, 'write')
  493. await sandbox.files.write(temporary, content, {
  494. metadata: { [VERSION_METADATA_KEY]: versionId },
  495. ...signalOpts(signal),
  496. })
  497. assertNotAborted(signal, 'write')
  498. const mode = existing === undefined ? 0o600 : existing.mode & 0o777
  499. await sandbox.commands.run(
  500. `chmod ${mode.toString(8)} -- ${quoteE2BShellArg(temporary)}`,
  501. commandOpts(signal),
  502. )
  503. assertNotAborted(signal, 'write')
  504. let committed: EntryInfo
  505. if (createIfAbsent) {
  506. const staged = await sandbox.files.getInfo(temporary, signalOpts(signal))
  507. assertNotAborted(signal, 'write')
  508. const targetArg = quoteE2BShellArg(targetPath)
  509. const publication = await sandbox.commands.run(
  510. `if ln -T -- ${quoteE2BShellArg(temporary)} ${targetArg}; then printf created; elif test -e ${targetArg} || test -L ${targetArg}; then printf exists; else exit 1; fi`,
  511. commandOpts(undefined),
  512. )
  513. if (publication.stdout === 'exists') {
  514. throw new FsError(
  515. `cannot overwrite existing "${target.displayPath}" without reading it first`,
  516. 'FS_NOT_OBSERVED',
  517. )
  518. }
  519. if (publication.stdout !== 'created') {
  520. throw new Error('guarded create returned an invalid publication result')
  521. }
  522. committed = { ...staged, name: posix.basename(targetPath), path: targetPath }
  523. } else {
  524. committed = await sandbox.files.rename(temporary, targetPath)
  525. }
  526. try {
  527. await sandbox.files.remove(stagingDirectory)
  528. } catch (_committedStagingCleanupFailure) {
  529. // The target is already committed; an empty private directory cannot turn that write into a failure.
  530. }
  531. return entryVersion(committed)
  532. } catch (error: unknown) {
  533. if (stagingDirectoryCreated) {
  534. try {
  535. await sandbox.files.remove(stagingDirectory)
  536. } catch (_stagingDirectoryAlreadyAbsentOrCleanupFailed) {
  537. // Only the private staging directory is swallowed; the original failure owns the operation.
  538. }
  539. }
  540. throw mapError(error, 'write', target.displayPath, signal)
  541. }
  542. }
  543. }
  544. export default E2BFileSystem