diff --git a/.changeset/ripe-ties-hang.md b/.changeset/ripe-ties-hang.md new file mode 100644 index 0000000000..9998096fcf --- /dev/null +++ b/.changeset/ripe-ties-hang.md @@ -0,0 +1,6 @@ +--- +'@e2b/python-sdk': patch +'e2b': patch +--- + +added gzip support for sandbox file/upload download diff --git a/packages/js-sdk/src/index.ts b/packages/js-sdk/src/index.ts index 17d4a97527..fbde63f9fe 100644 --- a/packages/js-sdk/src/index.ts +++ b/packages/js-sdk/src/index.ts @@ -22,7 +22,13 @@ export type { Logger } from './logs' export { getSignature } from './sandbox/signature' export { FileType } from './sandbox/filesystem' -export type { WriteInfo, EntryInfo, Filesystem } from './sandbox/filesystem' +export type { + WriteInfo, + EntryInfo, + Filesystem, + FilesystemEncodingOpts, + FilesystemFormatOpts, +} from './sandbox/filesystem' export { FilesystemEventType } from './sandbox/filesystem/watchHandle' export type { FilesystemEvent, diff --git a/packages/js-sdk/src/sandbox/filesystem/index.ts b/packages/js-sdk/src/sandbox/filesystem/index.ts index e57bb8d122..25ea8f6396 100644 --- a/packages/js-sdk/src/sandbox/filesystem/index.ts +++ b/packages/js-sdk/src/sandbox/filesystem/index.ts @@ -32,7 +32,7 @@ import { ENVD_VERSION_RECURSIVE_WATCH, } from '../../envd/versions' import { InvalidArgumentError, TemplateError } from '../../errors' -import { toBlob } from '../../utils' +import { toUploadBody } from '../../utils' /** * Sandbox filesystem object information. @@ -138,6 +138,29 @@ export interface FilesystemRequestOpts user?: Username } +/** + * Options for gzip compression of the request/response body. + */ +export interface FilesystemEncodingOpts { + /** + * When true, uploads will be gzip-compressed and downloads + * will request gzip-encoded responses. + */ + gzip?: boolean +} + +/** + * Options for the format of the file content returned by read operations. + */ +export interface FilesystemFormatOpts { + /** + * Format of the file content. + * + * @default 'text' + */ + format?: 'text' | 'bytes' | 'blob' | 'stream' +} + export interface FilesystemListOpts extends FilesystemRequestOpts { /** * Depth of the directory to list. @@ -196,7 +219,7 @@ export class Filesystem { */ async read( path: string, - opts?: FilesystemRequestOpts & { format?: 'text' } + opts?: FilesystemRequestOpts & FilesystemEncodingOpts & { format?: 'text' } ): Promise /** * Read file content as a `Uint8Array`. @@ -211,7 +234,7 @@ export class Filesystem { */ async read( path: string, - opts?: FilesystemRequestOpts & { format: 'bytes' } + opts?: FilesystemRequestOpts & FilesystemEncodingOpts & { format: 'bytes' } ): Promise /** * Read file content as a `Blob`. @@ -226,7 +249,7 @@ export class Filesystem { */ async read( path: string, - opts?: FilesystemRequestOpts & { format: 'blob' } + opts?: FilesystemRequestOpts & FilesystemEncodingOpts & { format: 'blob' } ): Promise /** * Read file content as a `ReadableStream`. @@ -241,13 +264,11 @@ export class Filesystem { */ async read( path: string, - opts?: FilesystemRequestOpts & { format: 'stream' } + opts?: FilesystemRequestOpts & FilesystemEncodingOpts & { format: 'stream' } ): Promise> async read( path: string, - opts?: FilesystemRequestOpts & { - format?: 'text' | 'stream' | 'bytes' | 'blob' - } + opts?: FilesystemRequestOpts & FilesystemEncodingOpts & FilesystemFormatOpts ): Promise { const format = opts?.format ?? 'text' @@ -259,6 +280,11 @@ export class Filesystem { user = defaultUsername } + const headers: Record = {} + if (opts?.gzip) { + headers['Accept-Encoding'] = 'gzip' + } + const res = await this.envdApi.api.GET('/files', { params: { query: { @@ -268,6 +294,7 @@ export class Filesystem { }, parseAs: format === 'bytes' ? 'arrayBuffer' : format, signal: this.connectionConfig.getSignal(opts?.requestTimeoutMs), + headers, }) const err = await handleEnvdApiError(res) @@ -306,11 +333,11 @@ export class Filesystem { async write( path: string, data: string | ArrayBuffer | Blob | ReadableStream, - opts?: FilesystemRequestOpts + opts?: FilesystemRequestOpts & FilesystemEncodingOpts ): Promise async write( files: WriteEntry[], - opts?: FilesystemRequestOpts + opts?: FilesystemRequestOpts & FilesystemEncodingOpts ): Promise async write( pathOrFiles: string | WriteEntry[], @@ -319,8 +346,8 @@ export class Filesystem { | ArrayBuffer | Blob | ReadableStream - | FilesystemRequestOpts, - opts?: FilesystemRequestOpts + | (FilesystemRequestOpts & FilesystemEncodingOpts), + opts?: FilesystemRequestOpts & FilesystemEncodingOpts ): Promise { if (typeof pathOrFiles !== 'string' && !Array.isArray(pathOrFiles)) { throw new Error('Path or files are required') @@ -336,7 +363,7 @@ export class Filesystem { typeof pathOrFiles === 'string' ? { path: pathOrFiles, - writeOpts: opts as FilesystemRequestOpts, + writeOpts: opts as FilesystemRequestOpts & FilesystemEncodingOpts, writeFiles: [ { data: dataOrOpts as @@ -349,17 +376,14 @@ export class Filesystem { } : { path: undefined, - writeOpts: dataOrOpts as FilesystemRequestOpts, + writeOpts: dataOrOpts as FilesystemRequestOpts & + FilesystemEncodingOpts, writeFiles: pathOrFiles as WriteEntry[], } if (writeFiles.length === 0) return [] as WriteInfo[] - const formData = new FormData() - for (let i = 0; i < writeFiles.length; i++) { - const file = writeFiles[i] - formData.append('file', await toBlob(file.data), writeFiles[i].path) - } + const useGzip = writeOpts?.gzip === true let user = writeOpts?.user if ( @@ -369,29 +393,43 @@ export class Filesystem { user = defaultUsername } - const res = await this.envdApi.api.POST('/files', { - params: { - query: { - path, - username: user, - }, - }, - bodySerializer: () => formData, - signal: this.connectionConfig.getSignal(opts?.requestTimeoutMs), - body: {}, - }) + const results: WriteInfo[] = [] + for (const file of writeFiles) { + const filePath = (file as WriteEntry).path ?? path - const err = await handleEnvdApiError(res) - if (err) { - throw err - } + const headers: Record = { + 'Content-Type': 'application/octet-stream', + } - const files = res.data as WriteInfo[] - if (!files) { - throw new Error('Expected to receive information about written file') + if (useGzip) { + headers['Content-Encoding'] = 'gzip' + } + const body = toUploadBody(file.data, useGzip) + + const res = await this.envdApi.api.POST('/files', { + params: { + query: { + path: filePath, + username: user, + }, + }, + bodySerializer: () => body, + signal: this.connectionConfig.getSignal(writeOpts?.requestTimeoutMs), + body: {}, + headers, + }) + + const err = await handleEnvdApiError(res) + if (err) throw err + + const fileInfos = res.data as WriteInfo[] + if (!fileInfos) { + throw new Error('Expected to receive information about written file') + } + results.push(...fileInfos) } - return files.length === 1 && path ? files[0] : files + return results.length === 1 && path ? results[0] : results } /** @@ -411,7 +449,7 @@ export class Filesystem { */ async writeFiles( files: WriteEntry[], - opts?: FilesystemRequestOpts + opts?: FilesystemRequestOpts & FilesystemEncodingOpts ): Promise { return this.write(files, opts) as Promise } diff --git a/packages/js-sdk/src/utils.ts b/packages/js-sdk/src/utils.ts index 92c925b8db..a544a3d20b 100644 --- a/packages/js-sdk/src/utils.ts +++ b/packages/js-sdk/src/utils.ts @@ -108,19 +108,25 @@ export async function wait(ms: number) { } /** - * Convert data to a Blob, avoiding unnecessary conversions when possible. + * Prepare data for upload as a BodyInit, optionally gzip-compressed. + * Streams data directly without buffering into memory. */ -export function toBlob( - data: string | ArrayBuffer | Blob | ReadableStream -): Blob | Promise { - // Already a Blob - use directly - if (data instanceof Blob) { - return data +export function toUploadBody( + data: string | ArrayBuffer | Blob | ReadableStream, + gzip?: boolean +): BodyInit { + if (gzip) { + const stream = + data instanceof ReadableStream + ? data + : data instanceof Blob + ? data.stream() + : new Blob([data]).stream() + return stream.pipeThrough(new CompressionStream('gzip')) } - // String or ArrayBuffer - create Blob - if (typeof data === 'string' || data instanceof ArrayBuffer) { - return new Blob([data]) + + if (data instanceof ReadableStream || data instanceof Blob) { + return data } - // ReadableStream - must consume to get Blob - return new Response(data).blob() + return new Blob([data]) } diff --git a/packages/js-sdk/tests/sandbox/files/contentEncoding.test.ts b/packages/js-sdk/tests/sandbox/files/contentEncoding.test.ts new file mode 100644 index 0000000000..2135cdc430 --- /dev/null +++ b/packages/js-sdk/tests/sandbox/files/contentEncoding.test.ts @@ -0,0 +1,94 @@ +import { assert } from 'vitest' + +import { WriteEntry } from '../../../src/sandbox/filesystem' +import { isDebug, sandboxTest } from '../../setup.js' + +sandboxTest( + 'write and read file with gzip content encoding', + async ({ sandbox }) => { + const filename = 'test_gzip_write.txt' + const content = 'This is a test file with gzip encoding.' + + const info = await sandbox.files.write(filename, content, { + gzip: true, + }) + assert.equal(info.name, filename) + assert.equal(info.type, 'file') + assert.equal(info.path, `/home/user/${filename}`) + + const readContent = await sandbox.files.read(filename, { + gzip: true, + }) + assert.equal(readContent, content) + + if (isDebug) { + await sandbox.files.remove(filename) + } + } +) + +sandboxTest( + 'write with gzip and read without encoding', + async ({ sandbox }) => { + const filename = 'test_gzip_write_plain_read.txt' + const content = 'Written with gzip, read without.' + + await sandbox.files.write(filename, content, { + gzip: true, + }) + + const readContent = await sandbox.files.read(filename) + assert.equal(readContent, content) + + if (isDebug) { + await sandbox.files.remove(filename) + } + } +) + +sandboxTest('writeFiles with gzip content encoding', async ({ sandbox }) => { + const files: WriteEntry[] = [ + { path: 'gzip_multi_1.txt', data: 'File 1 content' }, + { path: 'gzip_multi_2.txt', data: 'File 2 content' }, + { path: 'gzip_multi_3.txt', data: 'File 3 content' }, + ] + + const infos = await sandbox.files.writeFiles(files, { + gzip: true, + }) + + assert.equal(infos.length, files.length) + + for (let i = 0; i < files.length; i++) { + const readContent = await sandbox.files.read(files[i].path) + assert.equal(readContent, files[i].data) + } + + if (isDebug) { + for (const file of files) { + await sandbox.files.remove(file.path) + } + } +}) + +sandboxTest( + 'read file as bytes with gzip content encoding', + async ({ sandbox }) => { + const filename = 'test_gzip_bytes.txt' + const content = 'Binary content with gzip.' + + await sandbox.files.write(filename, content) + + const readBytes = await sandbox.files.read(filename, { + format: 'bytes', + gzip: true, + }) + assert.instanceOf(readBytes, Uint8Array) + const decoded = new TextDecoder().decode(readBytes) + assert.equal(decoded, content) + + if (isDebug) { + await sandbox.files.remove(filename) + } + } +) diff --git a/packages/python-sdk/e2b/sandbox/filesystem/filesystem.py b/packages/python-sdk/e2b/sandbox/filesystem/filesystem.py index ff6c235745..971daf5a4f 100644 --- a/packages/python-sdk/e2b/sandbox/filesystem/filesystem.py +++ b/packages/python-sdk/e2b/sandbox/filesystem/filesystem.py @@ -1,7 +1,10 @@ +import gzip +import zlib from dataclasses import dataclass from datetime import datetime from enum import Enum -from typing import IO, Optional, Union, TypedDict +from io import IOBase, TextIOBase +from typing import IO, Iterator, Optional, Union, TypedDict from e2b.envd.filesystem import filesystem_pb2 @@ -92,3 +95,38 @@ class WriteEntry(TypedDict): path: str data: Union[str, bytes, IO] + + +def to_upload_body( + data: Union[str, bytes, IO], + use_gzip: bool = False, +) -> Union[bytes, IOBase, Iterator[bytes]]: + """Prepare file data for upload, optionally gzip-compressed. + Streams IOBase data directly without buffering into memory.""" + if isinstance(data, str): + raw = data.encode("utf-8") + return gzip.compress(raw) if use_gzip else raw + elif isinstance(data, bytes): + return gzip.compress(data) if use_gzip else data + elif isinstance(data, TextIOBase): + raw = data.read().encode("utf-8") + return gzip.compress(raw) if use_gzip else raw + elif isinstance(data, IOBase): + if use_gzip: + return _gzip_stream(data) + return data + else: + raise TypeError(f"Unsupported data type: {type(data)}") + + +def _gzip_stream(source: IOBase, chunk_size: int = 65536) -> Iterator[bytes]: + """Stream-compress an IOBase through gzip without buffering the whole file.""" + compressor = zlib.compressobj(wbits=31) # 31 = gzip format + while True: + chunk = source.read(chunk_size) + if not chunk: + break + compressed = compressor.compress(chunk) + if compressed: + yield compressed + yield compressor.flush() diff --git a/packages/python-sdk/e2b/sandbox_async/filesystem/filesystem.py b/packages/python-sdk/e2b/sandbox_async/filesystem/filesystem.py index d363553eeb..9c6e6bc4ba 100644 --- a/packages/python-sdk/e2b/sandbox_async/filesystem/filesystem.py +++ b/packages/python-sdk/e2b/sandbox_async/filesystem/filesystem.py @@ -1,9 +1,8 @@ import httpcore import httpx -from io import IOBase, TextIOBase from packaging.version import Version from typing import AsyncIterator, IO, List, Literal, Optional, overload, Union -from e2b.sandbox.filesystem.filesystem import WriteEntry +from e2b.sandbox.filesystem.filesystem import WriteEntry, to_upload_body import e2b_connect as connect from e2b.connection_config import ( ConnectionConfig, @@ -62,6 +61,7 @@ async def read( format: Literal["text"] = "text", user: Optional[Username] = None, request_timeout: Optional[float] = None, + gzip: bool = False, ) -> str: """ Read file content as a `str`. @@ -70,6 +70,7 @@ async def read( :param user: Run the operation as this user :param format: Format of the file content—`text` by default :param request_timeout: Timeout for the request in **seconds** + :param gzip: Use gzip compression for the request :return: File content as a `str` """ @@ -82,6 +83,7 @@ async def read( format: Literal["bytes"], user: Optional[Username] = None, request_timeout: Optional[float] = None, + gzip: bool = False, ) -> bytearray: """ Read file content as a `bytearray`. @@ -90,6 +92,7 @@ async def read( :param user: Run the operation as this user :param format: Format of the file content—`bytes` :param request_timeout: Timeout for the request in **seconds** + :param gzip: Use gzip compression for the request :return: File content as a `bytearray` """ @@ -102,6 +105,7 @@ async def read( format: Literal["stream"], user: Optional[Username] = None, request_timeout: Optional[float] = None, + gzip: bool = False, ) -> AsyncIterator[bytes]: """ Read file content as a `AsyncIterator[bytes]`. @@ -110,6 +114,7 @@ async def read( :param user: Run the operation as this user :param format: Format of the file content—`stream` :param request_timeout: Timeout for the request in **seconds** + :param gzip: Use gzip compression for the request :return: File content as an `AsyncIterator[bytes]` """ @@ -121,6 +126,7 @@ async def read( format: Literal["text", "bytes", "stream"] = "text", user: Optional[Username] = None, request_timeout: Optional[float] = None, + gzip: bool = False, ): username = user if username is None and self._envd_version < ENVD_DEFAULT_USER: @@ -130,9 +136,14 @@ async def read( if username: params["username"] = username + headers = {} + if gzip: + headers["Accept-Encoding"] = "gzip" + r = await self._envd_api.get( ENVD_API_FILES_ROUTE, params=params, + headers=headers, timeout=self._connection_config.get_request_timeout(request_timeout), ) @@ -153,6 +164,7 @@ async def write( data: Union[str, bytes, IO], user: Optional[Username] = None, request_timeout: Optional[float] = None, + gzip: bool = False, ) -> WriteInfo: """ Write content to a file on the path. @@ -164,11 +176,15 @@ async def write( :param data: Data to write to the file, can be a `str`, `bytes`, or `IO`. :param user: Run the operation as this user :param request_timeout: Timeout for the request in **seconds** + :param gzip: Use gzip compression for the request :return: Information about the written file """ result = await self.write_files( - [WriteEntry(path=path, data=data)], user, request_timeout + [WriteEntry(path=path, data=data)], + user, + request_timeout, + gzip, ) if len(result) != 1: @@ -181,6 +197,7 @@ async def write_files( files: List[WriteEntry], user: Optional[Username] = None, request_timeout: Optional[float] = None, + gzip: bool = False, ) -> List[WriteInfo]: """ Writes multiple files. @@ -193,6 +210,7 @@ async def write_files( :param files: list of files to write as `WriteEntry` objects, each containing `path` and `data` :param user: Run the operation as this user :param request_timeout: Timeout for the request + :param gzip: Use gzip compression for the request :return: Information about the written files """ username = user @@ -202,48 +220,40 @@ async def write_files( params = {} if username: params["username"] = username - if len(files) == 1: - params["path"] = files[0]["path"] - - # Prepare the files for the multipart/form-data request - httpx_files = [] - for file in files: - file_path, file_data = file["path"], file["data"] - if isinstance(file_data, (str, bytes)): - # str and bytes can be passed directly - httpx_files.append(("file", (file_path, file_data))) - elif isinstance(file_data, TextIOBase): - # Text streams must be read first - httpx_files.append(("file", (file_path, file_data.read()))) - elif isinstance(file_data, IOBase): - # Binary streams can be passed directly - httpx_files.append(("file", (file_path, file_data))) - else: - raise InvalidArgumentException( - f"Unsupported data type for file {file_path}" - ) - # Allow passing empty list of files - if len(httpx_files) == 0: + if len(files) == 0: return [] - r = await self._envd_api.post( - ENVD_API_FILES_ROUTE, - files=httpx_files, - params=params, - timeout=self._connection_config.get_request_timeout(request_timeout), - ) + use_gzip = gzip + results = [] - err = await ahandle_envd_api_exception(r) - if err: - raise err + for file in files: + file_path, file_data = file["path"], file["data"] - write_files = r.json() + headers = {"Content-Type": "application/octet-stream"} + if use_gzip: + headers["Content-Encoding"] = "gzip" - if not isinstance(write_files, list) or len(write_files) == 0: - raise SandboxException("Expected to receive information about written file") + content = to_upload_body(file_data, use_gzip) + + r = await self._envd_api.post( + ENVD_API_FILES_ROUTE, + params={**params, "path": file_path}, + headers=headers, + content=content, + timeout=self._connection_config.get_request_timeout(request_timeout), + ) + err = await ahandle_envd_api_exception(r) + if err: + raise err + file_infos = r.json() + if not isinstance(file_infos, list) or len(file_infos) == 0: + raise SandboxException( + "Expected to receive information about written file" + ) + results.extend([WriteInfo(**f) for f in file_infos]) - return [WriteInfo(**file) for file in write_files] + return results async def list( self, diff --git a/packages/python-sdk/e2b/sandbox_sync/filesystem/filesystem.py b/packages/python-sdk/e2b/sandbox_sync/filesystem/filesystem.py index 7f23d11338..6913104292 100644 --- a/packages/python-sdk/e2b/sandbox_sync/filesystem/filesystem.py +++ b/packages/python-sdk/e2b/sandbox_sync/filesystem/filesystem.py @@ -1,7 +1,6 @@ -from io import IOBase, TextIOBase from typing import IO, Iterator, List, Literal, Optional, overload, Union -from e2b.sandbox.filesystem.filesystem import WriteEntry +from e2b.sandbox.filesystem.filesystem import WriteEntry, to_upload_body import e2b_connect import httpcore @@ -63,6 +62,7 @@ def read( format: Literal["text"] = "text", user: Optional[Username] = None, request_timeout: Optional[float] = None, + gzip: bool = False, ) -> str: """ Read file content as a `str`. @@ -71,6 +71,7 @@ def read( :param user: Run the operation as this user :param format: Format of the file content—`text` by default :param request_timeout: Timeout for the request in **seconds** + :param gzip: Use gzip compression for the request :return: File content as a `str` """ @@ -83,6 +84,7 @@ def read( format: Literal["bytes"], user: Optional[Username] = None, request_timeout: Optional[float] = None, + gzip: bool = False, ) -> bytearray: """ Read file content as a `bytearray`. @@ -91,6 +93,7 @@ def read( :param user: Run the operation as this user :param format: Format of the file content—`bytes` :param request_timeout: Timeout for the request in **seconds** + :param gzip: Use gzip compression for the request :return: File content as a `bytearray` """ @@ -103,6 +106,7 @@ def read( format: Literal["stream"], user: Optional[Username] = None, request_timeout: Optional[float] = None, + gzip: bool = False, ) -> Iterator[bytes]: """ Read file content as a `Iterator[bytes]`. @@ -111,6 +115,7 @@ def read( :param user: Run the operation as this user :param format: Format of the file content—`stream` :param request_timeout: Timeout for the request in **seconds** + :param gzip: Use gzip compression for the request :return: File content as an `Iterator[bytes]` """ @@ -122,6 +127,7 @@ def read( format: Literal["text", "bytes", "stream"] = "text", user: Optional[Username] = None, request_timeout: Optional[float] = None, + gzip: bool = False, ): username = user if username is None and self._envd_version < ENVD_DEFAULT_USER: @@ -131,9 +137,14 @@ def read( if username: params["username"] = username + headers = {} + if gzip: + headers["Accept-Encoding"] = "gzip" + r = self._envd_api.get( ENVD_API_FILES_ROUTE, params=params, + headers=headers, timeout=self._connection_config.get_request_timeout(request_timeout), ) @@ -154,6 +165,7 @@ def write( data: Union[str, bytes, IO], user: Optional[Username] = None, request_timeout: Optional[float] = None, + gzip: bool = False, ) -> WriteInfo: """ Write content to a file on the path. @@ -165,6 +177,7 @@ def write( :param data: Data to write to the file, can be a `str`, `bytes`, or `IO`. :param user: Run the operation as this user :param request_timeout: Timeout for the request in **seconds** + :param gzip: Use gzip compression for the request :return: Information about the written file """ @@ -172,6 +185,7 @@ def write( [WriteEntry(path=path, data=data)], user=user, request_timeout=request_timeout, + gzip=gzip, ) if len(result) != 1: @@ -184,6 +198,7 @@ def write_files( files: List[WriteEntry], user: Optional[Username] = None, request_timeout: Optional[float] = None, + gzip: bool = False, ) -> List[WriteInfo]: """ Writes a list of files to the filesystem. @@ -194,6 +209,7 @@ def write_files( :param files: list of files to write as `WriteEntry` objects, each containing `path` and `data` :param user: Run the operation as this user :param request_timeout: Timeout for the request + :param gzip: Use gzip compression for the request :return: Information about the written files """ username = user @@ -203,48 +219,40 @@ def write_files( params = {} if username: params["username"] = username - if len(files) == 1: - params["path"] = files[0]["path"] - - # Prepare the files for the multipart/form-data request - httpx_files = [] - for file in files: - file_path, file_data = file["path"], file["data"] - if isinstance(file_data, (str, bytes)): - # str and bytes can be passed directly - httpx_files.append(("file", (file_path, file_data))) - elif isinstance(file_data, TextIOBase): - # Text streams must be read first - httpx_files.append(("file", (file_path, file_data.read()))) - elif isinstance(file_data, IOBase): - # Binary streams can be passed directly - httpx_files.append(("file", (file_path, file_data))) - else: - raise InvalidArgumentException( - f"Unsupported data type for file {file_path}" - ) - # Allow passing empty list of files - if len(httpx_files) == 0: + if len(files) == 0: return [] - r = self._envd_api.post( - ENVD_API_FILES_ROUTE, - files=httpx_files, - params=params, - timeout=self._connection_config.get_request_timeout(request_timeout), - ) + use_gzip = gzip + results = [] - err = handle_envd_api_exception(r) - if err: - raise err + for file in files: + file_path, file_data = file["path"], file["data"] + + headers = {"Content-Type": "application/octet-stream"} + if use_gzip: + headers["Content-Encoding"] = "gzip" - write_files = r.json() + content = to_upload_body(file_data, use_gzip) - if not isinstance(write_files, list) or len(write_files) == 0: - raise SandboxException("Expected to receive information about written file") + r = self._envd_api.post( + ENVD_API_FILES_ROUTE, + params={**params, "path": file_path}, + headers=headers, + content=content, + timeout=self._connection_config.get_request_timeout(request_timeout), + ) + err = handle_envd_api_exception(r) + if err: + raise err + file_infos = r.json() + if not isinstance(file_infos, list) or len(file_infos) == 0: + raise SandboxException( + "Expected to receive information about written file" + ) + results.extend([WriteInfo(**f) for f in file_infos]) - return [WriteInfo(**file) for file in write_files] + return results def list( self, diff --git a/packages/python-sdk/tests/async/sandbox_async/files/test_content_encoding.py b/packages/python-sdk/tests/async/sandbox_async/files/test_content_encoding.py new file mode 100644 index 0000000000..c6b2df85ac --- /dev/null +++ b/packages/python-sdk/tests/async/sandbox_async/files/test_content_encoding.py @@ -0,0 +1,62 @@ +from e2b import AsyncSandbox +from e2b.sandbox.filesystem.filesystem import WriteEntry + + +async def test_write_and_read_with_gzip(async_sandbox: AsyncSandbox, debug): + filename = "test_gzip_write.txt" + content = "This is a test file with gzip encoding." + + info = await async_sandbox.files.write(filename, content, gzip=True) + assert info.path == f"/home/user/{filename}" + + read_content = await async_sandbox.files.read(filename, gzip=True) + assert read_content == content + + if debug: + await async_sandbox.files.remove(filename) + + +async def test_write_gzip_read_plain(async_sandbox: AsyncSandbox, debug): + filename = "test_gzip_write_plain_read.txt" + content = "Written with gzip, read without." + + await async_sandbox.files.write(filename, content, gzip=True) + + read_content = await async_sandbox.files.read(filename) + assert read_content == content + + if debug: + await async_sandbox.files.remove(filename) + + +async def test_write_files_with_gzip(async_sandbox: AsyncSandbox, debug): + files = [ + WriteEntry(path="gzip_multi_1.txt", data="File 1 content"), + WriteEntry(path="gzip_multi_2.txt", data="File 2 content"), + WriteEntry(path="gzip_multi_3.txt", data="File 3 content"), + ] + + infos = await async_sandbox.files.write_files(files, gzip=True) + assert len(infos) == len(files) + + for file in files: + read_content = await async_sandbox.files.read(file["path"]) + assert read_content == file["data"] + + if debug: + for file in files: + await async_sandbox.files.remove(file["path"]) + + +async def test_read_bytes_with_gzip(async_sandbox: AsyncSandbox, debug): + filename = "test_gzip_bytes.txt" + content = "Binary content with gzip." + + await async_sandbox.files.write(filename, content) + + read_bytes = await async_sandbox.files.read(filename, format="bytes", gzip=True) + assert isinstance(read_bytes, bytearray) + assert read_bytes.decode("utf-8") == content + + if debug: + await async_sandbox.files.remove(filename) diff --git a/packages/python-sdk/tests/sync/sandbox_sync/files/test_content_encoding.py b/packages/python-sdk/tests/sync/sandbox_sync/files/test_content_encoding.py new file mode 100644 index 0000000000..3d8b68bc08 --- /dev/null +++ b/packages/python-sdk/tests/sync/sandbox_sync/files/test_content_encoding.py @@ -0,0 +1,61 @@ +from e2b.sandbox.filesystem.filesystem import WriteEntry + + +def test_write_and_read_with_gzip(sandbox, debug): + filename = "test_gzip_write.txt" + content = "This is a test file with gzip encoding." + + info = sandbox.files.write(filename, content, gzip=True) + assert info.path == f"/home/user/{filename}" + + read_content = sandbox.files.read(filename, gzip=True) + assert read_content == content + + if debug: + sandbox.files.remove(filename) + + +def test_write_gzip_read_plain(sandbox, debug): + filename = "test_gzip_write_plain_read.txt" + content = "Written with gzip, read without." + + sandbox.files.write(filename, content, gzip=True) + + read_content = sandbox.files.read(filename) + assert read_content == content + + if debug: + sandbox.files.remove(filename) + + +def test_write_files_with_gzip(sandbox, debug): + files = [ + WriteEntry(path="gzip_multi_1.txt", data="File 1 content"), + WriteEntry(path="gzip_multi_2.txt", data="File 2 content"), + WriteEntry(path="gzip_multi_3.txt", data="File 3 content"), + ] + + infos = sandbox.files.write_files(files, gzip=True) + assert len(infos) == len(files) + + for i, file in enumerate(files): + read_content = sandbox.files.read(file["path"]) + assert read_content == file["data"] + + if debug: + for file in files: + sandbox.files.remove(file["path"]) + + +def test_read_bytes_with_gzip(sandbox, debug): + filename = "test_gzip_bytes.txt" + content = "Binary content with gzip." + + sandbox.files.write(filename, content) + + read_bytes = sandbox.files.read(filename, format="bytes", gzip=True) + assert isinstance(read_bytes, bytearray) + assert read_bytes.decode("utf-8") == content + + if debug: + sandbox.files.remove(filename)