diff --git a/.changeset/new-berries-heal.md b/.changeset/new-berries-heal.md new file mode 100644 index 000000000..bf9e0601a --- /dev/null +++ b/.changeset/new-berries-heal.md @@ -0,0 +1,6 @@ +--- +'@e2b/python-sdk': minor +'e2b': minor +--- + +the Filesytem write method is overloaded to also allow passing in an array of files diff --git a/apps/web/src/app/(docs)/docs/filesystem/read-write/page.mdx b/apps/web/src/app/(docs)/docs/filesystem/read-write/page.mdx index c61c586c0..5f17a03cf 100644 --- a/apps/web/src/app/(docs)/docs/filesystem/read-write/page.mdx +++ b/apps/web/src/app/(docs)/docs/filesystem/read-write/page.mdx @@ -2,7 +2,7 @@ ## Reading files -You can read files from the sandbox filesystem using the `files.reado()` method. +You can read files from the sandbox filesystem using the `files.read()` method. ```js @@ -18,20 +18,48 @@ file_content = sandbox.files.read('/path/to/file') ``` -## Writing files +## Writing single files -You can write files to the sandbox filesystem using the `files.write()` method. +You can write signle files to the sandbox filesystem using the `files.write()` method. ```js import { Sandbox } from '@e2b/code-interpreter' const sandbox = await Sandbox.create() + await sandbox.files.write('/path/to/file', 'file content') ``` ```python from e2b_code_interpreter import Sandbox sandbox = Sandbox() + await sandbox.files.write('/path/to/file', 'file content') ``` + +## Writing multiple files + +You can also write multiple files to the sandbox filesystem using the `files.write()` method. + + +```js +import { Sandbox } from '@e2b/code-interpreter' +const sandbox = await Sandbox.create() + +await sandbox.files.write([ + { path: '/path/to/a', data: 'file content' }, + { path: '/another/path/to/b', data: 'file content' } +]) +``` +```python +from e2b_code_interpreter import Sandbox + +sandbox = Sandbox() + +await sandbox.files.write([ + { "path": "/path/to/a", "data": "file content" }, + { "path": "another/path/to/b", "data": "file content" } +]) +``` + \ No newline at end of file diff --git a/apps/web/src/app/(docs)/docs/filesystem/upload/page.mdx b/apps/web/src/app/(docs)/docs/filesystem/upload/page.mdx index 1b16f2668..b0d7877d7 100644 --- a/apps/web/src/app/(docs)/docs/filesystem/upload/page.mdx +++ b/apps/web/src/app/(docs)/docs/filesystem/upload/page.mdx @@ -2,6 +2,8 @@ You can upload data to the sandbox using the `files.write()` method. +## Upload single file + ```js import fs from 'fs' @@ -22,6 +24,89 @@ sandbox = Sandbox() # Read file from local filesystem with open("path/to/local/file", "rb") as file: # Upload file to sandbox - sandbox.files.write("/path/in/sandbox", file) + sandbox.files.write("/path/in/sandbox", file) ``` + +## Upload directory / multiple files + + +```js +const fs = require('fs'); +const path = require('path'); + +import { Sandbox } from '@e2b/code-interpreter' + +const sandbox = await Sandbox.create() + +// Read all files in the directory and store their paths and contents in an array +const readDirectoryFiles = (directoryPath) => { + // Read all files in the local directory + const files = fs.readdirSync(directoryPath); + + // Map files to objects with path and data + const filesArray = files + .filter(file => { + const fullPath = path.join(directoryPath, file); + // Skip if it's a directory + return fs.statSync(fullPath).isFile(); + }) + .map(file => { + const filePath = path.join(directoryPath, file); + + // Read the content of each file + return { + path: filePath, + data: fs.readFileSync(filePath, 'utf8') + }; + }); + + return filesArray; +}; + +// Usage example +const files = readDirectoryContents('/local/dir'); +console.log(files); +// [ +// { path: '/local/dir/file1.txt', data: 'File 1 contents...' }, +// { path: '/local/dir/file2.txt', data: 'File 2 contents...' }, +// ... +// ] + +await sandbox.files.write(files) +``` +```python +import os +from e2b_code_interpreter import Sandbox + +sandbox = Sandbox() + +def read_directory_files(directory_path): + files = [] + + # Iterate through all files in the directory + for filename in os.listdir(directory_path): + file_path = os.path.join(directory_path, filename) + + # Skip if it's a directory + if os.path.isfile(file_path): + # Read file contents in binary mode + with open(file_path, "rb") as file: + files.append({ + 'path': file_path, + 'data': file.read() + }) + + return files + +files = read_directory_files("/local/dir") +print(files) +# [ +# {"'path": "/local/dir/file1.txt", "data": "File 1 contents..." }, +# { "path": "/local/dir/file2.txt", "data": "File 2 contents..." }, +# ... +# ] + +sandbox.files.write(files) +``` + \ No newline at end of file diff --git a/packages/js-sdk/src/index.ts b/packages/js-sdk/src/index.ts index 61541c133..17e6e59d8 100644 --- a/packages/js-sdk/src/index.ts +++ b/packages/js-sdk/src/index.ts @@ -1,26 +1,23 @@ export { ApiClient } from './api' export type { components, paths } from './api' +export { ConnectionConfig } from './connectionConfig' +export type { ConnectionOpts, Username } from './connectionConfig' export { AuthenticationError, - SandboxError, - TimeoutError, - NotFoundError, - NotEnoughSpaceError, InvalidArgumentError, + NotEnoughSpaceError, + NotFoundError, + SandboxError, TemplateError, + TimeoutError, } from './errors' -export { ConnectionConfig } from './connectionConfig' export type { Logger } from './logs' -export type { ConnectionOpts, Username } from './connectionConfig' -export { FilesystemEventType } from './sandbox/filesystem/watchHandle' -export type { - FilesystemEvent, - WatchHandle, -} from './sandbox/filesystem/watchHandle' -export type { EntryInfo, Filesystem, WatchOpts } from './sandbox/filesystem' export { FileType } from './sandbox/filesystem' +export type { EntryInfo, Filesystem } from './sandbox/filesystem' +export { FilesystemEventType } from './sandbox/filesystem/watchHandle' +export type { FilesystemEvent, WatchHandle } from './sandbox/filesystem/watchHandle' export { CommandExitError } from './sandbox/commands/commandHandle' export type { @@ -41,8 +38,8 @@ export type { Pty, } from './sandbox/commands' -export type { SandboxInfo } from './sandbox/sandboxApi' export type { SandboxOpts } from './sandbox' -import { Sandbox } from './sandbox' +export type { SandboxInfo } from './sandbox/sandboxApi' export { Sandbox } +import { Sandbox } from './sandbox' export default Sandbox diff --git a/packages/js-sdk/src/sandbox/filesystem/index.ts b/packages/js-sdk/src/sandbox/filesystem/index.ts index f65003ae8..68f8c3c4b 100644 --- a/packages/js-sdk/src/sandbox/filesystem/index.ts +++ b/packages/js-sdk/src/sandbox/filesystem/index.ts @@ -7,11 +7,11 @@ import { } from '@connectrpc/connect' import { ConnectionConfig, - defaultUsername, - Username, ConnectionOpts, - KEEPALIVE_PING_INTERVAL_SEC, + defaultUsername, KEEPALIVE_PING_HEADER, + KEEPALIVE_PING_INTERVAL_SEC, + Username, } from '../../connectionConfig' import { handleEnvdApiError, handleWatchDirStartEvent } from '../../envd/api' @@ -20,7 +20,7 @@ import { authenticationHeader, handleRpcError } from '../../envd/rpc' import { EnvdApiClient } from '../../envd/api' import { FileType as FsFileType, Filesystem as FilesystemService } from '../../envd/filesystem/filesystem_pb' -import { WatchHandle, FilesystemEvent } from './watchHandle' +import { FilesystemEvent, WatchHandle } from './watchHandle' /** * Sandbox filesystem object information. @@ -54,6 +54,11 @@ export const enum FileType { DIR = 'dir', } +export type WriteEntry = { + path: string + data: string | ArrayBuffer | Blob | ReadableStream +} + function mapFileType(fileType: FsFileType) { switch (fileType) { case FsFileType.DIRECTORY: @@ -220,26 +225,53 @@ export class Filesystem { * * @returns information about the written file */ + async write(path: string, data: string | ArrayBuffer | Blob | ReadableStream, opts?: FilesystemRequestOpts): Promise + async write(files: WriteEntry[], opts?: FilesystemRequestOpts): Promise async write( - path: string, - data: string | ArrayBuffer | Blob | ReadableStream, + pathOrFiles: string | WriteEntry[], + dataOrOpts?: string | ArrayBuffer | Blob | ReadableStream | FilesystemRequestOpts, opts?: FilesystemRequestOpts - ): Promise { - const blob = await new Response(data).blob() + ): Promise { + if (typeof pathOrFiles !== 'string' && !Array.isArray(pathOrFiles)) { + throw new Error('Path or files are required') + } + + if (typeof pathOrFiles === 'string' && Array.isArray(dataOrOpts)) { + throw new Error( + 'Cannot specify both path and array of files. You have to specify either path and data for a single file or an array for multiple files.' + ) + } + + const { path, writeOpts, writeFiles } = + typeof pathOrFiles === 'string' + ? { + path: pathOrFiles, + writeOpts: opts as FilesystemRequestOpts, + writeFiles: [{ data: dataOrOpts as string | ArrayBuffer | Blob | ReadableStream }], + } + : { path: undefined, writeOpts: dataOrOpts as FilesystemRequestOpts, writeFiles: pathOrFiles as WriteEntry[] } + + if (writeFiles.length === 0) return [] as EntryInfo[] + + const blobs = await Promise.all(writeFiles.map((f) => new Response(f.data).blob())) const res = await this.envdApi.api.POST('/files', { params: { query: { path, - username: opts?.user || defaultUsername, + username: writeOpts?.user || defaultUsername, }, }, bodySerializer() { - const fd = new FormData() - - fd.append('file', blob) - - return fd + return blobs.reduce((fd, blob, i) => { + // Important: RFC 7578, Section 4.2 requires that if a filename is provided, + // the directory path information must not be used. + // BUT in our case we need to use the directory path information with a custom + // muktipart part name getter in envd. + fd.append('file', blob, writeFiles[i].path) + + return fd + }, new FormData()) }, body: {}, headers: { @@ -253,12 +285,12 @@ export class Filesystem { throw err } - const files = res.data - if (!files || files.length === 0) { + const files = res.data as EntryInfo[] + if (!files) { throw new Error('Expected to receive information about written file') } - return files[0] as EntryInfo + return files.length === 1 && path ? files[0] : files } /** @@ -338,11 +370,7 @@ export class Filesystem { * * @returns information about renamed file or directory. */ - async rename( - oldPath: string, - newPath: string, - opts?: FilesystemRequestOpts - ): Promise { + async rename(oldPath: string, newPath: string, opts?: FilesystemRequestOpts): Promise { try { const res = await this.rpc.move( { @@ -432,10 +460,12 @@ export class Filesystem { async watchDir( path: string, onEvent: (event: FilesystemEvent) => void | Promise, - opts?: WatchOpts + opts?: FilesystemRequestOpts & { + timeout?: number + onExit?: (err?: Error) => void | Promise + } ): Promise { - const requestTimeoutMs = - opts?.requestTimeoutMs ?? this.connectionConfig.requestTimeoutMs + const requestTimeoutMs = opts?.requestTimeoutMs ?? this.connectionConfig.requestTimeoutMs const controller = new AbortController() @@ -462,12 +492,7 @@ export class Filesystem { clearTimeout(reqTimeout) - return new WatchHandle( - () => controller.abort(), - events, - onEvent, - opts?.onExit - ) + return new WatchHandle(() => controller.abort(), events, onEvent, opts?.onExit) } catch (err) { throw handleRpcError(err) } diff --git a/packages/js-sdk/tests/sandbox/files/write.test.ts b/packages/js-sdk/tests/sandbox/files/write.test.ts index 72d101d41..b2baa3b44 100644 --- a/packages/js-sdk/tests/sandbox/files/write.test.ts +++ b/packages/js-sdk/tests/sandbox/files/write.test.ts @@ -1,12 +1,123 @@ -import { assert } from 'vitest' +import path from 'path' +import { assert, onTestFinished } from 'vitest' +import { WriteEntry } from '../../../src/sandbox/filesystem/index.js' import { sandboxTest } from '../../setup.js' +sandboxTest('write file', async ({ sandbox }) => { + const filename = 'test_write.txt' + const content = 'This is a test file.' + + // Attempt to write with undefined path and content + await sandbox.files + // @ts-ignore + .write(undefined, content) + .then((e) => { + assert.isUndefined(e) + }) + .catch((err) => { + assert.instanceOf(err, Error) + assert.include(err.message, 'Path or files are required') + }) + + const info = await sandbox.files.write(filename, content) + assert.isFalse(Array.isArray(info)) + assert.equal(info.name, filename) + assert.equal(info.type, 'file') + assert.equal(info.path, `/home/user/${filename}`) + + const exists = await sandbox.files.exists(filename) + assert.isTrue(exists) + const readContent = await sandbox.files.read(filename) + assert.equal(readContent, content) +}) + +sandboxTest('write multiple files', async ({ sandbox }) => { + // Attempt to write with empty files array + const emptyInfo = await sandbox.files.write([]) + assert.isTrue(Array.isArray(emptyInfo)) + assert.equal(emptyInfo.length, 0) + + // Attempt to write with undefined path and file array + await sandbox.files + // @ts-ignore + .write(undefined, [{ path: 'one_test_file.txt', data: 'This is a test file.' }]) + .then((e) => { + assert.isUndefined(e) + }) + .catch((err) => { + assert.instanceOf(err, Error) + assert.include(err.message, 'Path or files are required') + }) + + // Attempt to write with path and file array + await sandbox.files + // @ts-ignore + .write('/path/to/file', [{ path: 'one_test_file.txt', data: 'This is a test file.' }]) + .then((e) => { + assert.isUndefined(e) + }) + .catch((err) => { + assert.instanceOf(err, Error) + assert.include( + err.message, + 'Cannot specify both path and array of files. You have to specify either path and data for a single file or an array for multiple files.' + ) + }) + + // Attempt to write with one file in array + const info = await sandbox.files.write([{ path: 'one_test_file.txt', data: 'This is a test file.' }]) + assert.isTrue(Array.isArray(info)) + assert.equal(info[0].name, 'one_test_file.txt') + assert.equal(info[0].type, 'file') + assert.equal(info[0].path, `/home/user/one_test_file.txt`) + + // Attempt to write with multiple files in array + let files: WriteEntry[] = [] + + for (let i = 0; i < 10; i++) { + let path = '' + if (i % 2 == 0) { + path = `/${i}/multi_test_file${i}.txt` + } else { + path = `/home/user/multi_test_file${i}.txt` + } + + onTestFinished(async () => await sandbox.files.remove(path)) + + files.push({ + path: path, + data: `This is a test file ${i}.`, + }) + } + + const infos = await sandbox.files.write(files) + + assert.isTrue(Array.isArray(infos)) + assert.equal(infos.length, files.length) + + // Attempt to write with multiple files in array + for (let i = 0; i < files.length; i++) { + const file = files[i] + const info = infos[i] + + assert.equal(info.name, path.basename(file.path)) + assert.equal(info.path, file.path) + assert.equal(info.type, 'file') + + const exists = await sandbox.files.exists(file.path) + assert.isTrue(exists) + const readContent = await sandbox.files.read(file.path) + assert.equal(readContent, file.data) + } +}) + sandboxTest('write file', async ({ sandbox }) => { const filename = 'test_write.txt' const content = 'This is a test file.' const info = await sandbox.files.write(filename, content) + assert.isFalse(Array.isArray(info)) assert.equal(info.name, filename) assert.equal(info.type, 'file') assert.equal(info.path, `/home/user/${filename}`) diff --git a/packages/python-sdk/e2b/exceptions.py b/packages/python-sdk/e2b/exceptions.py index d0214936b..d519972b4 100644 --- a/packages/python-sdk/e2b/exceptions.py +++ b/packages/python-sdk/e2b/exceptions.py @@ -30,10 +30,10 @@ class TimeoutException(SandboxException): """ Raised when a timeout occurs. - The [unavailable] exception type is caused by sandbox timeout.\n - The [canceled] exception type is caused by exceeding request timeout.\n - The [deadline_exceeded] exception type is caused by exceeding the timeout for process, watch, etc.\n - The [unknown] exception type is sometimes caused by the sandbox timeout when the request is not processed correctly.\n + The `unavailable` exception type is caused by sandbox timeout.\n + The `canceled` exception type is caused by exceeding request timeout.\n + The `deadline_exceeded` exception type is caused by exceeding the timeout for process, watch, etc.\n + The `unknown` exception type is sometimes caused by the sandbox timeout when the request is not processed correctly.\n """ pass diff --git a/packages/python-sdk/e2b/sandbox/filesystem/filesystem.py b/packages/python-sdk/e2b/sandbox/filesystem/filesystem.py index 9e04a3975..2f6717014 100644 --- a/packages/python-sdk/e2b/sandbox/filesystem/filesystem.py +++ b/packages/python-sdk/e2b/sandbox/filesystem/filesystem.py @@ -1,6 +1,6 @@ -from enum import Enum from dataclasses import dataclass -from typing import Optional +from enum import Enum +from typing import IO, Optional, Union from e2b.envd.filesystem import filesystem_pb2 @@ -45,3 +45,13 @@ class EntryInfo: """ Path to the filesystem object. """ + + +dataclass +class WriteEntry: + """ + Contains path and data of the file to be written to the filesystem. + """ + + path: str + data: Union[str, bytes, IO] diff --git a/packages/python-sdk/e2b/sandbox_async/filesystem/filesystem.py b/packages/python-sdk/e2b/sandbox_async/filesystem/filesystem.py index 216147c78..adef7a66f 100644 --- a/packages/python-sdk/e2b/sandbox_async/filesystem/filesystem.py +++ b/packages/python-sdk/e2b/sandbox_async/filesystem/filesystem.py @@ -1,5 +1,6 @@ from io import TextIOBase -from typing import IO, AsyncIterator, List, Literal, Optional, Union, overload +from typing import AsyncIterator, IO, List, Literal, Optional, overload, Union +from e2b.sandbox.filesystem.filesystem import WriteEntry import e2b_connect as connect import httpcore @@ -129,6 +130,7 @@ async def read( elif format == "stream": return r.aiter_bytes() + @overload async def write( self, path: str, @@ -152,13 +154,69 @@ async def write( :return: Information about the written file """ - if isinstance(data, TextIOBase): - data = data.read().encode() + + @overload + async def write( + self, + files: List[WriteEntry], + user: Optional[Username] = "user", + request_timeout: Optional[float] = None, + ) -> List[EntryInfo]: + """ + Writes multiple files. + + :param files: list of files to write + :param user: Run the operation as this user + :param request_timeout: Timeout for the request + :return: Information about the written files + """ + + async def write( + self, + path_or_files: Union[str, List[WriteEntry]], + data_or_user: Union[str, bytes, IO, Username] = "user", + user_or_request_timeout: Optional[Union[float, Username]] = None, + request_timeout_or_none: Optional[float] = None + ) -> Union[EntryInfo, List[EntryInfo]]: + """ + Writes content to a file on the path. + When writing to a file that doesn't exist, the file will get created. + When writing to a file that already exists, the file will get overwritten. + When writing to a file that's in a directory that doesn't exist, you'll get an error. + """ + path, write_files, user, request_timeout = None, [], "user", None + if isinstance(path_or_files, str): + if isinstance(data_or_user, list): + raise Exception("Cannot specify both path and array of files. You have to specify either path and data for a single file or an array for multiple files.") + path, write_files, user, request_timeout = \ + path_or_files, [{"path": path_or_files, "data": data_or_user}], user_or_request_timeout or "user", request_timeout_or_none + else: + if path_or_files is None: + raise Exception("Path or files are required") + path, write_files, user, request_timeout = \ + None, path_or_files, data_or_user, user_or_request_timeout + + # Prepare the files for the multipart/form-data request + httpx_files = [] + for file in write_files: + file_path, file_data = file['path'], file['data'] + if isinstance(file_data, str) or isinstance(file_data, bytes): + httpx_files.append(('file', (file_path, file_data))) + elif isinstance(file_data, TextIOBase): + httpx_files.append(('file', (file_path, file_data.read()))) + else: + raise ValueError(f"Unsupported data type for file {file_path}") + + # Allow passing empty list of files + if len(httpx_files) == 0: return [] + + params = {"username": user} + if path is not None: params["path"] = path r = await self._envd_api.post( ENVD_API_FILES_ROUTE, - files={"file": data}, - params={"path": path, "username": user}, + files=httpx_files, + params=params, timeout=self._connection_config.get_request_timeout(request_timeout), ) @@ -166,13 +224,17 @@ async def write( if err: raise err - files = r.json() + write_files = r.json() - if not isinstance(files, list) or len(files) == 0: + if not isinstance(write_files, list) or len(write_files) == 0: raise Exception("Expected to receive information about written file") - file = files[0] - return EntryInfo(**file) + if len(write_files) == 1 and path: + file = write_files[0] + return EntryInfo(**file) + else: + return [EntryInfo(**file) for file in write_files] + 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 094d82a71..5c73111fa 100644 --- a/packages/python-sdk/e2b/sandbox_sync/filesystem/filesystem.py +++ b/packages/python-sdk/e2b/sandbox_sync/filesystem/filesystem.py @@ -1,5 +1,6 @@ from io import TextIOBase -from typing import IO, Iterator, List, Literal, Optional, Union, overload +from typing import IO, Iterator, List, Literal, Optional, overload, Union +from e2b.sandbox.filesystem.filesystem import WriteEntry import e2b_connect import httpcore @@ -126,6 +127,7 @@ def read( elif format == "stream": return r.iter_bytes() + @overload def write( self, path: str, @@ -149,13 +151,66 @@ def write( :return: Information about the written file """ - if isinstance(data, TextIOBase): - data = data.read().encode() + + @overload + def write( + self, + files: List[WriteEntry], + user: Optional[Username] = "user", + request_timeout: Optional[float] = None, + ) -> List[EntryInfo]: + """ + Writes a list of files to the filesystem. + When writing to a file that doesn't exist, the file will get created. + When writing to a file that already exists, the file will get overwritten. + When writing to a file that's in a directory that doesn't exist, you'll get an error. + + :param files: list of files to write + :param user: Run the operation as this user + :param request_timeout: Timeout for the request + :return: Information about the written files + """ + + def write( + self, + path_or_files: Union[str, List[WriteEntry]], + data_or_user: Union[str, bytes, IO, Username] = "user", + user_or_request_timeout: Optional[Union[float, Username]] = None, + request_timeout_or_none: Optional[float] = None + ) -> Union[EntryInfo, List[EntryInfo]]: + path, write_files, user, request_timeout = None, [], "user", None + if isinstance(path_or_files, str): + if isinstance(data_or_user, list): + raise Exception("Cannot specify both path and array of files. You have to specify either path and data for a single file or an array for multiple files.") + path, write_files, user, request_timeout = \ + path_or_files, [{"path": path_or_files, "data": data_or_user}], user_or_request_timeout or "user", request_timeout_or_none + else: + if path_or_files is None: + raise Exception("Path or files are required") + path, write_files, user, request_timeout = \ + None, path_or_files, data_or_user, user_or_request_timeout + + # Prepare the files for the multipart/form-data request + httpx_files = [] + for file in write_files: + file_path, file_data = file['path'], file['data'] + if isinstance(file_data, str) or isinstance(file_data, bytes): + httpx_files.append(('file', (file_path, file_data))) + elif isinstance(file_data, TextIOBase): + httpx_files.append(('file', (file_path, file_data.read()))) + else: + raise ValueError(f"Unsupported data type for file {file_path}") + + # Allow passing empty list of files + if len(httpx_files) == 0: return [] + + params = {"username": user} + if path is not None: params["path"] = path r = self._envd_api.post( ENVD_API_FILES_ROUTE, - files={"file": data}, - params={"path": path, "username": user}, + files=httpx_files, + params=params, timeout=self._connection_config.get_request_timeout(request_timeout), ) @@ -163,13 +218,16 @@ def write( if err: raise err - files = r.json() + write_files = r.json() - if not isinstance(files, list) or len(files) == 0: + if not isinstance(write_files, list) or len(write_files) == 0: raise Exception("Expected to receive information about written file") - file = files[0] - return EntryInfo(**file) + if len(write_files) == 1 and path: + file = write_files[0] + return EntryInfo(**file) + else: + return [EntryInfo(**file) for file in write_files] def list( self, diff --git a/packages/python-sdk/tests/async/sandbox_async/files/test_write.py b/packages/python-sdk/tests/async/sandbox_async/files/test_write.py index bdfbf514a..b897f3673 100644 --- a/packages/python-sdk/tests/async/sandbox_async/files/test_write.py +++ b/packages/python-sdk/tests/async/sandbox_async/files/test_write.py @@ -1,10 +1,16 @@ from e2b import AsyncSandbox - +from e2b.sandbox_async.filesystem.filesystem import EntryInfo async def test_write_file(async_sandbox: AsyncSandbox): filename = "test_write.txt" content = "This is a test file." + # Attempt to write without path + try: + await async_sandbox.files.write(None, content) + except Exception as e: + assert "Path or files are required" in str(e) + info = await async_sandbox.files.write(filename, content) assert info.path == f"/home/user/{filename}" @@ -14,6 +20,55 @@ async def test_write_file(async_sandbox: AsyncSandbox): read_content = await async_sandbox.files.read(filename) assert read_content == content +async def test_write_multiple_files(async_sandbox: AsyncSandbox): + # Attempt to write with empty files array + empty_info = await async_sandbox.files.write([]) + assert isinstance(empty_info, list) + assert len(empty_info) == 0 + + # Attempt to write with None path and empty files array + try: + await async_sandbox.files.write(None, []) + except Exception as e: + assert "Path or files are required" in str(e) + + # Attempt to write with path and file array + try: + await async_sandbox.files.write("/path/to/file", [{ "path": "one_test_file.txt", "data": "This is a test file." }]) + except Exception as e: + assert "Cannot specify both path and array of files. You have to specify either path and data for a single file or an array for multiple files." in str(e) + + # Attempt to write with one file in array + info = await async_sandbox.files.write([{ "path": "one_test_file.txt", "data": "This is a test file." }]) + assert isinstance(info, list) + assert len(info) == 1 + info = info[0] + assert isinstance(info, EntryInfo) + assert info.path == "/home/user/one_test_file.txt" + exists = await async_sandbox.files.exists(info.path) + assert exists + + read_content = await async_sandbox.files.read(info.path) + assert read_content == "This is a test file." + + # Attempt to write with multiple files in array + files = [] + for i in range(10): + path = f"test_write_{i}.txt" + content = f"This is a test file {i}." + files.append({"path": path, "data": content}) + + infos = await async_sandbox.files.write(files) + assert isinstance(infos, list) + assert len(infos) == len(files) + for i, info in enumerate(infos): + assert isinstance(info, EntryInfo) + assert info.path == f"/home/user/test_write_{i}.txt" + exists = await async_sandbox.files.exists(path) + assert exists + + read_content = await async_sandbox.files.read(info.path) + assert read_content == files[i]["data"] async def test_overwrite_file(async_sandbox: AsyncSandbox): filename = "test_overwrite.txt" diff --git a/packages/python-sdk/tests/sync/sandbox_sync/files/test_watch.py b/packages/python-sdk/tests/sync/sandbox_sync/files/test_watch.py index e9fc5faf1..bc4b0afa8 100644 --- a/packages/python-sdk/tests/sync/sandbox_sync/files/test_watch.py +++ b/packages/python-sdk/tests/sync/sandbox_sync/files/test_watch.py @@ -13,8 +13,8 @@ def test_watch_directory_changes(sandbox: Sandbox): sandbox.files.write(f"{dirname}/{filename}", content) events = handle.get_new_events() - assert len(events) == 3 - assert events[0].type == FilesystemEventType.CREATE + assert len(events) >= 3 + assert events[0].type == FilesystemEventType.WRITE assert events[0].name == filename assert events[1].type == FilesystemEventType.CHMOD assert events[1].name == filename diff --git a/packages/python-sdk/tests/sync/sandbox_sync/files/test_write.py b/packages/python-sdk/tests/sync/sandbox_sync/files/test_write.py index ba0390ee1..5a3638765 100644 --- a/packages/python-sdk/tests/sync/sandbox_sync/files/test_write.py +++ b/packages/python-sdk/tests/sync/sandbox_sync/files/test_write.py @@ -1,7 +1,15 @@ +from e2b.sandbox.filesystem.filesystem import EntryInfo + def test_write_file(sandbox): filename = "test_write.txt" content = "This is a test file." + # Attempt to write without path + try: + sandbox.files.write(None, content) + except Exception as e: + assert "Path or files are required" in str(e) + info = sandbox.files.write(filename, content) assert info.path == f"/home/user/{filename}" @@ -11,6 +19,55 @@ def test_write_file(sandbox): read_content = sandbox.files.read(filename) assert read_content == content +def test_write_multiple_files(sandbox): + # Attempt to write with empty files array + empty_info = sandbox.files.write([]) + assert isinstance(empty_info, list) + assert len(empty_info) == 0 + + # Attempt to write with None path and empty files array + try: + sandbox.files.write(None, []) + except Exception as e: + assert "Path or files are required" in str(e) + + # Attempt to write with path and file array + try: + sandbox.files.write("/path/to/file", [{ "path": "one_test_file.txt", "data": "This is a test file." }]) + except Exception as e: + assert "Cannot specify both path and array of files. You have to specify either path and data for a single file or an array for multiple files." in str(e) + + # Attempt to write with one file in array + info = sandbox.files.write([{ "path": "one_test_file.txt", "data": "This is a test file." }]) + assert isinstance(info, list) + assert len(info) == 1 + info = info[0] + assert isinstance(info, EntryInfo) + assert info.path == "/home/user/one_test_file.txt" + exists = sandbox.files.exists(info.path) + assert exists + + read_content = sandbox.files.read(info.path) + assert read_content == "This is a test file." + + # Attempt to write with multiple files in array + files = [] + for i in range(10): + path = f"test_write_{i}.txt" + content = f"This is a test file {i}." + files.append({"path": path, "data": content}) + + infos = sandbox.files.write(files) + assert isinstance(infos, list) + assert len(infos) == len(files) + for i, info in enumerate(infos): + assert isinstance(info, EntryInfo) + assert info.path == f"/home/user/test_write_{i}.txt" + exists = sandbox.files.exists(path) + assert exists + + read_content = sandbox.files.read(info.path) + assert read_content == files[i]["data"] def test_overwrite_file(sandbox): filename = "test_overwrite.txt"