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"