Skip to content

Commit 4af9b4c

Browse files
authored
Avoid reading files on upload (#1098)
<!-- CURSOR_SUMMARY --> > [!NOTE] > Optimizes file uploads in both JS and Python SDKs to avoid unnecessary reads and add broader input support. > > - JS SDK: `Filesystem.write`/`writeFiles` now build a single `FormData` using `toBlob` (new util) to pass `string`/`ArrayBuffer`/`Blob`/`ReadableStream` without pre-reading; updated tests add `ReadableStream` coverage > - Python SDK (sync/async): `write_files` accepts `str`/`bytes` directly, reads `TextIOBase`, and passes `IOBase` (binary) streams through without reading; new tests cover `BytesIO` and `StringIO` > - Changeset: patch bumps for `@e2b/python-sdk` and `e2b` > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 45516e3. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 168d9ed commit 4af9b4c

8 files changed

Lines changed: 131 additions & 21 deletions

File tree

.changeset/yellow-plants-matter.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@e2b/python-sdk': patch
3+
'e2b': patch
4+
---
5+
6+
avoid reading files on upload

packages/js-sdk/src/sandbox/filesystem/index.ts

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
ENVD_VERSION_RECURSIVE_WATCH,
3333
} from '../../envd/versions'
3434
import { InvalidArgumentError, TemplateError } from '../../errors'
35+
import { toBlob } from '../../utils'
3536

3637
/**
3738
* Sandbox filesystem object information.
@@ -354,9 +355,11 @@ export class Filesystem {
354355

355356
if (writeFiles.length === 0) return [] as WriteInfo[]
356357

357-
const blobs = await Promise.all(
358-
writeFiles.map((f) => new Response(f.data).blob())
359-
)
358+
const formData = new FormData()
359+
for (let i = 0; i < writeFiles.length; i++) {
360+
const file = writeFiles[i]
361+
formData.append('file', await toBlob(file.data), writeFiles[i].path)
362+
}
360363

361364
let user = writeOpts?.user
362365
if (
@@ -373,17 +376,7 @@ export class Filesystem {
373376
username: user,
374377
},
375378
},
376-
bodySerializer() {
377-
return blobs.reduce((fd, blob, i) => {
378-
// Important: RFC 7578, Section 4.2 requires that if a filename is provided,
379-
// the directory path information must not be used.
380-
// BUT in our case we need to use the directory path information with a custom
381-
// multipart part name getter in envd.
382-
fd.append('file', blob, writeFiles[i].path)
383-
384-
return fd
385-
}, new FormData())
386-
},
379+
bodySerializer: () => formData,
387380
signal: this.connectionConfig.getSignal(opts?.requestTimeoutMs),
388381
body: {},
389382
})

packages/js-sdk/src/utils.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,21 @@ export function stripAnsi(text: string): string {
106106
export async function wait(ms: number) {
107107
return new Promise((resolve) => setTimeout(resolve, ms))
108108
}
109+
110+
/**
111+
* Convert data to a Blob, avoiding unnecessary conversions when possible.
112+
*/
113+
export function toBlob(
114+
data: string | ArrayBuffer | Blob | ReadableStream
115+
): Blob | Promise<Blob> {
116+
// Already a Blob - use directly
117+
if (data instanceof Blob) {
118+
return data
119+
}
120+
// String or ArrayBuffer - create Blob
121+
if (typeof data === 'string' || data instanceof ArrayBuffer) {
122+
return new Blob([data])
123+
}
124+
// ReadableStream - must consume to get Blob
125+
return new Response(data).blob()
126+
}

packages/js-sdk/tests/sandbox/files/write.test.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,16 +228,25 @@ sandboxTest('writeFiles with different data types', async ({ sandbox }) => {
228228
const textData = 'Text string data'
229229
const arrayBufferData = new TextEncoder().encode('ArrayBuffer data').buffer
230230
const blobData = new Blob(['Blob data'], { type: 'text/plain' })
231+
const streamContent = 'ReadableStream data'
232+
const encoder = new TextEncoder()
233+
const streamData = new ReadableStream({
234+
start(controller) {
235+
controller.enqueue(encoder.encode(streamContent))
236+
controller.close()
237+
},
238+
})
231239

232240
const files: WriteEntry[] = [
233241
{ path: 'writefiles_text.txt', data: textData },
234242
{ path: 'writefiles_arraybuffer.txt', data: arrayBufferData },
235243
{ path: 'writefiles_blob.txt', data: blobData },
244+
{ path: 'writefiles_stream.txt', data: streamData },
236245
]
237246

238247
const infos = await sandbox.files.writeFiles(files)
239248

240-
assert.equal(infos.length, 3)
249+
assert.equal(infos.length, 4)
241250

242251
// Verify text file
243252
const textContent = await sandbox.files.read('writefiles_text.txt')
@@ -253,6 +262,10 @@ sandboxTest('writeFiles with different data types', async ({ sandbox }) => {
253262
const blobContent = await sandbox.files.read('writefiles_blob.txt')
254263
assert.equal(blobContent, 'Blob data')
255264

265+
// Verify ReadableStream file
266+
const streamFileContent = await sandbox.files.read('writefiles_stream.txt')
267+
assert.equal(streamFileContent, streamContent)
268+
256269
if (isDebug) {
257270
for (const file of files) {
258271
await sandbox.files.remove(file.path)

packages/python-sdk/e2b/sandbox_async/filesystem/filesystem.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import httpcore
22
import httpx
3-
from io import IOBase
3+
from io import IOBase, TextIOBase
44
from packaging.version import Version
55
from typing import AsyncIterator, IO, List, Literal, Optional, overload, Union
66
from e2b.sandbox.filesystem.filesystem import WriteEntry
@@ -209,10 +209,15 @@ async def write_files(
209209
httpx_files = []
210210
for file in files:
211211
file_path, file_data = file["path"], file["data"]
212-
if isinstance(file_data, str) or isinstance(file_data, bytes):
212+
if isinstance(file_data, (str, bytes)):
213+
# str and bytes can be passed directly
213214
httpx_files.append(("file", (file_path, file_data)))
214-
elif isinstance(file_data, IOBase):
215+
elif isinstance(file_data, TextIOBase):
216+
# Text streams must be read first
215217
httpx_files.append(("file", (file_path, file_data.read())))
218+
elif isinstance(file_data, IOBase):
219+
# Binary streams can be passed directly
220+
httpx_files.append(("file", (file_path, file_data)))
216221
else:
217222
raise InvalidArgumentException(
218223
f"Unsupported data type for file {file_path}"

packages/python-sdk/e2b/sandbox_sync/filesystem/filesystem.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from io import IOBase
1+
from io import IOBase, TextIOBase
22
from typing import IO, Iterator, List, Literal, Optional, overload, Union
33

44
from e2b.sandbox.filesystem.filesystem import WriteEntry
@@ -210,10 +210,15 @@ def write_files(
210210
httpx_files = []
211211
for file in files:
212212
file_path, file_data = file["path"], file["data"]
213-
if isinstance(file_data, str) or isinstance(file_data, bytes):
213+
if isinstance(file_data, (str, bytes)):
214+
# str and bytes can be passed directly
214215
httpx_files.append(("file", (file_path, file_data)))
215-
elif isinstance(file_data, IOBase):
216+
elif isinstance(file_data, TextIOBase):
217+
# Text streams must be read first
216218
httpx_files.append(("file", (file_path, file_data.read())))
219+
elif isinstance(file_data, IOBase):
220+
# Binary streams can be passed directly
221+
httpx_files.append(("file", (file_path, file_data)))
217222
else:
218223
raise InvalidArgumentException(
219224
f"Unsupported data type for file {file_path}"

packages/python-sdk/tests/async/sandbox_async/files/test_write.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,39 @@ async def test_write_with_secured_envd(async_sandbox_factory):
138138

139139
read_content = await sbx.files.read(filename)
140140
assert read_content == content
141+
142+
143+
async def test_write_files_with_different_data_types(
144+
async_sandbox: AsyncSandbox, debug
145+
):
146+
text_data = "Text string data"
147+
bytes_data = b"Bytes data"
148+
bytes_io_data = io.BytesIO(b"BytesIO data")
149+
string_io_data = io.StringIO("StringIO data")
150+
151+
files = [
152+
WriteEntry(path="writefiles_text.txt", data=text_data),
153+
WriteEntry(path="writefiles_bytes.bin", data=bytes_data),
154+
WriteEntry(path="writefiles_bytesio.bin", data=bytes_io_data),
155+
WriteEntry(path="writefiles_stringio.txt", data=string_io_data),
156+
]
157+
158+
infos = await async_sandbox.files.write_files(files)
159+
160+
assert len(infos) == 4
161+
162+
text_content = await async_sandbox.files.read("writefiles_text.txt")
163+
assert text_content == text_data
164+
165+
bytes_content = await async_sandbox.files.read("writefiles_bytes.bin")
166+
assert bytes_content == "Bytes data"
167+
168+
bytes_io_content = await async_sandbox.files.read("writefiles_bytesio.bin")
169+
assert bytes_io_content == "BytesIO data"
170+
171+
string_io_content = await async_sandbox.files.read("writefiles_stringio.txt")
172+
assert string_io_content == "StringIO data"
173+
174+
if debug:
175+
for file in files:
176+
await async_sandbox.files.remove(file["path"])

packages/python-sdk/tests/sync/sandbox_sync/files/test_write.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,37 @@ def test_write_with_secured_envd(sandbox_factory):
138138

139139
read_content = sbx.files.read(filename)
140140
assert read_content == content
141+
142+
143+
def test_write_files_with_different_data_types(sandbox, debug):
144+
text_data = "Text string data"
145+
bytes_data = b"Bytes data"
146+
bytes_io_data = io.BytesIO(b"BytesIO data")
147+
string_io_data = io.StringIO("StringIO data")
148+
149+
files = [
150+
WriteEntry(path="writefiles_text.txt", data=text_data),
151+
WriteEntry(path="writefiles_bytes.bin", data=bytes_data),
152+
WriteEntry(path="writefiles_bytesio.bin", data=bytes_io_data),
153+
WriteEntry(path="writefiles_stringio.txt", data=string_io_data),
154+
]
155+
156+
infos = sandbox.files.write_files(files)
157+
158+
assert len(infos) == 4
159+
160+
text_content = sandbox.files.read("writefiles_text.txt")
161+
assert text_content == text_data
162+
163+
bytes_content = sandbox.files.read("writefiles_bytes.bin")
164+
assert bytes_content == "Bytes data"
165+
166+
bytes_io_content = sandbox.files.read("writefiles_bytesio.bin")
167+
assert bytes_io_content == "BytesIO data"
168+
169+
string_io_content = sandbox.files.read("writefiles_stringio.txt")
170+
assert string_io_content == "StringIO data"
171+
172+
if debug:
173+
for file in files:
174+
sandbox.files.remove(file["path"])

0 commit comments

Comments
 (0)