Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add multi-file write support to the js and python sdks #451

Open
wants to merge 35 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b2e80e1
Added multi-file write support
0div Oct 4, 2024
9e6bc17
address PR comments
0div Oct 4, 2024
966f0c7
[WIP] Cleanup
ValentaTomas Oct 5, 2024
eda88d0
address PR comments
0div Oct 7, 2024
a2d4ad7
boyscouting: fix some docstrings
0div Oct 7, 2024
a379a58
Add multi-file write support for python-sdk sync
0div Oct 7, 2024
608ef45
Use `@overload`
0div Oct 8, 2024
68ac038
merge beta
0div Oct 8, 2024
f010211
adapt multi file write tests for nested dirs
0div Oct 8, 2024
fbc4af4
allow passing empty array of files in python-sdk
0div Oct 8, 2024
2afb6d1
allow passing empty array of files in js-sdk
0div Oct 8, 2024
68e5efa
address PR comments
0div Oct 9, 2024
365af43
add extra tests to sandbox_sync write
0div Oct 9, 2024
7767d8f
updated js-sdk tests to check empty path behavior
0div Oct 9, 2024
f5cd1c0
add multifile upload to sanbox_async
0div Oct 9, 2024
834f84c
merge beta
0div Oct 10, 2024
2956a6e
better error messages in python-sdk
0div Oct 10, 2024
c277f9f
better error messages in js-sdk
0div Oct 10, 2024
86262f1
docstring for dataclass
0div Oct 10, 2024
97d0cc1
merge main
0div Dec 12, 2024
cf262ae
fix errors.ts comment
0div Dec 12, 2024
4ca2414
fixed typing syntax and watch tests
0div Dec 12, 2024
93dc1f9
update docs
0div Dec 12, 2024
741b329
add minor changeset
0div Dec 12, 2024
bbeeb04
upadte upload docs and improve read-write-docs
0div Dec 12, 2024
0e2a684
fix indentation in upload docs
0div Dec 13, 2024
b867fe4
Update apps/web/src/app/(docs)/docs/filesystem/read-write/page.mdx
0div Dec 13, 2024
e730810
Update apps/web/src/app/(docs)/docs/filesystem/read-write/page.mdx
0div Dec 13, 2024
d3b99fe
Update apps/web/src/app/(docs)/docs/filesystem/upload/page.mdx
0div Dec 13, 2024
424baec
Update apps/web/src/app/(docs)/docs/filesystem/upload/page.mdx
0div Dec 13, 2024
e73ad23
Update apps/web/src/app/(docs)/docs/filesystem/upload/page.mdx
0div Dec 13, 2024
d0fc09a
Update apps/web/src/app/(docs)/docs/filesystem/upload/page.mdx
0div Dec 13, 2024
1708cc0
Update apps/web/src/app/(docs)/docs/filesystem/upload/page.mdx
0div Dec 13, 2024
502c414
remove WriteData type in js-sdk
0div Dec 13, 2024
724fbc6
remove WriteData type in python-sdk
0div Dec 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/new-berries-heal.md
Original file line number Diff line number Diff line change
@@ -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
34 changes: 31 additions & 3 deletions apps/web/src/app/(docs)/docs/filesystem/read-write/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<CodeGroup>
```js
Expand All @@ -18,20 +18,48 @@ file_content = sandbox.files.read('/path/to/file')
```
</CodeGroup>

## 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.

<CodeGroup>
```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')
```
</CodeGroup>

## Writing multiple files

You can also write multiple files to the sandbox filesystem using the `files.write()` method.

<CodeGroup>
```js
import { Sandbox } from '@e2b/code-interpreter'
const sandbox = await Sandbox.create()

await sandbox.files.write([
{ path: "/path/to/a", data: "file content" },
0div marked this conversation as resolved.
Show resolved Hide resolved
{ path: "/another/path/to/b", data: "file content" }
0div marked this conversation as resolved.
Show resolved Hide resolved
])
```
```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" }
])
```
</CodeGroup>
85 changes: 85 additions & 0 deletions apps/web/src/app/(docs)/docs/filesystem/upload/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

You can upload data to the sandbox using the `files.write()` method.

## Upload single file

<CodeGroup>
```js
import fs from 'fs'
Expand All @@ -25,3 +27,86 @@ with open("path/to/local/file", "rb") as file:
sandbox.files.write("/path/in/sandbox", file)
```
</CodeGroup>

## Upload directory / multiple files

<CodeGroup>
```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');
0div marked this conversation as resolved.
Show resolved Hide resolved
print(files);
0div marked this conversation as resolved.
Show resolved Hide resolved
// [
0div marked this conversation as resolved.
Show resolved Hide resolved
// { 'path': '/local/dir/file1.txt', 'data': 'File 1 contents...' },
0div marked this conversation as resolved.
Show resolved Hide resolved
// { 'path': '/local/dir/file2.txt', 'data': 'File 2 contents...' },
// ...
// ]
0div marked this conversation as resolved.
Show resolved Hide resolved

sandbox.files.write(files)
```
</CodeGroup>
27 changes: 13 additions & 14 deletions packages/js-sdk/src/index.ts
Original file line number Diff line number Diff line change
@@ -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, WriteData } 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 {
Expand All @@ -41,8 +38,10 @@ export type {
Pty,
} from './sandbox/commands'

export type { SandboxInfo } from './sandbox/sandboxApi'
export type { Pty } from './sandbox/pty'

export type { SandboxOpts } from './sandbox'
import { Sandbox } from './sandbox'
export type { SandboxInfo } from './sandbox/sandboxApi'
export { Sandbox }
import { Sandbox } from './sandbox'
export default Sandbox
84 changes: 54 additions & 30 deletions packages/js-sdk/src/sandbox/filesystem/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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.
Expand Down Expand Up @@ -54,6 +54,13 @@ export const enum FileType {
DIR = 'dir',
}

export type WriteData = string | ArrayBuffer | Blob | ReadableStream
0div marked this conversation as resolved.
Show resolved Hide resolved

export type WriteEntry = {
path: string
data: WriteData
}

function mapFileType(fileType: FsFileType) {
switch (fileType) {
case FsFileType.DIRECTORY:
Expand Down Expand Up @@ -220,26 +227,53 @@ export class Filesystem {
*
* @returns information about the written file
*/
async write(path: string, data: WriteData, opts?: FilesystemRequestOpts): Promise<EntryInfo>
async write(files: WriteEntry[], opts?: FilesystemRequestOpts): Promise<EntryInfo[]>
async write(
path: string,
data: string | ArrayBuffer | Blob | ReadableStream,
pathOrFiles: string | WriteEntry[],
dataOrOpts?: WriteData | FilesystemRequestOpts,
opts?: FilesystemRequestOpts
): Promise<EntryInfo> {
const blob = await new Response(data).blob()
): Promise<EntryInfo | EntryInfo[]> {
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 WriteData }],
}
: { path: undefined, writeOpts: dataOrOpts as FilesystemRequestOpts, writeFiles: pathOrFiles as WriteEntry[] }

0div marked this conversation as resolved.
Show resolved Hide resolved
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,
0div marked this conversation as resolved.
Show resolved Hide resolved
// 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: {
Expand All @@ -253,12 +287,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
}

/**
Expand Down Expand Up @@ -338,11 +372,7 @@ export class Filesystem {
*
* @returns information about renamed file or directory.
*/
async rename(
oldPath: string,
newPath: string,
opts?: FilesystemRequestOpts
): Promise<EntryInfo> {
async rename(oldPath: string, newPath: string, opts?: FilesystemRequestOpts): Promise<EntryInfo> {
try {
const res = await this.rpc.move(
{
Expand Down Expand Up @@ -434,8 +464,7 @@ export class Filesystem {
onEvent: (event: FilesystemEvent) => void | Promise<void>,
opts?: WatchOpts
): Promise<WatchHandle> {
const requestTimeoutMs =
opts?.requestTimeoutMs ?? this.connectionConfig.requestTimeoutMs
const requestTimeoutMs = opts?.requestTimeoutMs ?? this.connectionConfig.requestTimeoutMs

const controller = new AbortController()

Expand All @@ -462,12 +491,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)
}
Expand Down
Loading
Loading