@@ -3,6 +3,7 @@ import { type IAgentClient } from "../AgentClient/agent-client-interface";
3
3
import { Disposable } from "../utils/disposable" ;
4
4
import { Emitter , type Event } from "../utils/event" ;
5
5
import { Tracer , SpanStatusCode } from "@opentelemetry/api" ;
6
+ import { zip } from "fflate" ;
6
7
7
8
export type FSStatResult = {
8
9
type : "file" | "directory" ;
@@ -39,6 +40,11 @@ export type Watcher = {
39
40
onEvent : Event < WatchEvent > ;
40
41
} ;
41
42
43
+ export type BatchWriteFile = {
44
+ path : string ;
45
+ content : string | Uint8Array ;
46
+ } ;
47
+
42
48
export class FileSystem {
43
49
private disposable = new Disposable ( ) ;
44
50
private tracer ?: Tracer ;
@@ -137,6 +143,97 @@ export class FileSystem {
137
143
) ;
138
144
}
139
145
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
+
140
237
/**
141
238
* Create a directory.
142
239
*/
0 commit comments