@@ -3,6 +3,7 @@ import { type IAgentClient } from "../AgentClient/agent-client-interface";
33import { Disposable } from "../utils/disposable" ;
44import { Emitter , type Event } from "../utils/event" ;
55import { Tracer , SpanStatusCode } from "@opentelemetry/api" ;
6+ import { zip } from "fflate" ;
67
78export type FSStatResult = {
89 type : "file" | "directory" ;
@@ -39,6 +40,11 @@ export type Watcher = {
3940 onEvent : Event < WatchEvent > ;
4041} ;
4142
43+ export type BatchWriteFile = {
44+ path : string ;
45+ content : string | Uint8Array ;
46+ } ;
47+
4248export class FileSystem {
4349 private disposable = new Disposable ( ) ;
4450 private tracer ?: Tracer ;
@@ -137,6 +143,97 @@ export class FileSystem {
137143 ) ;
138144 }
139145
146+ /**
147+ * Batch write multiple files by zipping them, uploading the zip, and extracting it on the sandbox.
148+ * This is more efficient than writing many files individually.
149+ * Files will be created/overwritten as needed.
150+ */
151+ async batchWrite ( files : BatchWriteFile [ ] ) : Promise < void > {
152+ return this . withSpan (
153+ "fs.batchWrite" ,
154+ {
155+ "fs.fileCount" : files . length ,
156+ } ,
157+ async ( ) => {
158+ if ( files . length === 0 ) {
159+ return ;
160+ }
161+
162+ // Create a zip file containing all the files
163+ const zipData : Record < string , Uint8Array > = { } ;
164+
165+ for ( const file of files ) {
166+ const content =
167+ typeof file . content === "string"
168+ ? new TextEncoder ( ) . encode ( file . content )
169+ : file . content ;
170+ zipData [ file . path ] = content ;
171+ }
172+
173+ // Create the zip using fflate
174+ const zipBytes = await new Promise < Uint8Array > ( ( resolve , reject ) => {
175+ zip ( zipData , ( err , data ) => {
176+ if ( err ) reject ( err ) ;
177+ else resolve ( data ) ;
178+ } ) ;
179+ } ) ;
180+
181+ // Write the zip file to a temporary location
182+ const tempZipPath = `/tmp/batch_write_${ Date . now ( ) } .zip` ;
183+ await this . writeFile ( tempZipPath , zipBytes ) ;
184+
185+ try {
186+ // Extract the zip file using unzip command
187+ const result = await this . agentClient . shells . create (
188+ this . agentClient . workspacePath ,
189+ { cols : 128 , rows : 24 } ,
190+ `cd ${ this . agentClient . workspacePath } && unzip -o ${ tempZipPath } ` ,
191+ "COMMAND" ,
192+ true
193+ ) ;
194+
195+ if ( result . status === "ERROR" || result . status === "KILLED" ) {
196+ throw new Error (
197+ `Failed to extract batch files: ${
198+ result . buffer ?. join ( "\n" ) || "Unknown error"
199+ } `
200+ ) ;
201+ }
202+
203+ // Wait for the command to complete if it's still running
204+ if ( result . status === "RUNNING" ) {
205+ // Wait for shell exit event
206+ await new Promise < void > ( ( resolve , reject ) => {
207+ const disposable = this . agentClient . shells . onShellExited (
208+ ( { shellId, exitCode } ) => {
209+ if ( shellId === result . shellId ) {
210+ disposable . dispose ( ) ;
211+ if ( exitCode === 0 ) {
212+ resolve ( ) ;
213+ } else {
214+ reject (
215+ new Error (
216+ `Unzip command failed with exit code ${ exitCode } `
217+ )
218+ ) ;
219+ }
220+ }
221+ }
222+ ) ;
223+ } ) ;
224+ }
225+ } finally {
226+ // Always clean up the temporary zip file, regardless of success or failure
227+ try {
228+ await this . remove ( tempZipPath ) ;
229+ } catch {
230+ // Ignore cleanup errors - file might already be deleted or not exist
231+ }
232+ }
233+ }
234+ ) ;
235+ }
236+
140237 /**
141238 * Create a directory.
142239 */
0 commit comments