Skip to content

Commit

Permalink
Add recursive directory watch (#488)
Browse files Browse the repository at this point in the history
Adds recursive directory watch to the SDK and documentation section.
Requires merging and deploying e2b-dev/infra#210
first.

### Proposed API change:
**JS**
```js
await sandbox.files.watchDir(dirname, handler, {
  recursive: true
})
```

**Python**
```py
sandbox.files.watch_dir(dirname, recursive=True)
```


---

### Notes

Python SDK was generated using buf.build/protocolbuffers/python:v28.3.
The runtime check was removed for now. Further tests might be required
to make sure the runtime is at least the generated version before
including the runtime check.
Changing the following in the file spec/envd/buf-python.gen.yaml
(generator) allows using different protoc-gen-python version:
From: `plugin: python`
To: `plugin: buf.build/protocolbuffers/python:v28.3`
  • Loading branch information
jakubno authored Jan 28, 2025
2 parents 1e1d398 + 13f0c78 commit 4fb4470
Show file tree
Hide file tree
Showing 26 changed files with 446 additions and 79 deletions.
6 changes: 6 additions & 0 deletions .changeset/good-peas-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@e2b/python-sdk': patch
'e2b': patch
---

Add recursive watch dir
49 changes: 49 additions & 0 deletions apps/web/src/app/(docs)/docs/filesystem/watch/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,52 @@ for event in events: # $HighlightLine
print(f"wrote to file {event.name}") # $HighlightLine
```
</CodeGroup>


## Recursive Watching

You can enable recursive watching using the parameter `recursive`.

<Note>
When rapidly creating new folders (e.g., deeply nested path of folders), events other than `CREATE` might not be emitted. To avoid this behavior, create the required folder structure in advance.
</Note>

<CodeGroup>
```js
import { Sandbox, FilesystemEventType } from '@e2b/code-interpreter'

const sandbox = await Sandbox.create()
const dirname = '/home/user'

// Start watching directory for changes
const handle = await sandbox.files.watchDir(dirname, async (event) => {
console.log(event)
if (event.type === FilesystemEventType.WRITE) {
console.log(`wrote to file ${event.name}`)
}
}, {
recursive: true // $HighlightLine
})

// Trigger file write event
await sandbox.files.write(`${dirname}/my-folder/my-file`, 'hello') // $HighlightLine
```
```python
from e2b_code_interpreter import Sandbox

sandbox = Sandbox()
dirname = '/home/user'

# Watch directory for changes
handle = sandbox.files.watch_dir(dirname, recursive=True) # $HighlightLine
# Trigger file write event
sandbox.files.write(f"{dirname}/my-folder/my-file", "hello") # $HighlightLine

# Retrieve the latest new events since the last `get_new_events()` call
events = handle.get_new_events()
for event in events:
print(event)
if event.type == FilesystemEventType.Write:
print(f"wrote to file {event.name}")
```
</CodeGroup>
12 changes: 11 additions & 1 deletion packages/js-sdk/src/envd/filesystem/filesystem_pb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf";
* Describes the file filesystem/filesystem.proto.
*/
export const file_filesystem_filesystem: GenFile = /*@__PURE__*/
fileDesc("ChtmaWxlc3lzdGVtL2ZpbGVzeXN0ZW0ucHJvdG8SCmZpbGVzeXN0ZW0iMgoLTW92ZVJlcXVlc3QSDgoGc291cmNlGAEgASgJEhMKC2Rlc3RpbmF0aW9uGAIgASgJIjQKDE1vdmVSZXNwb25zZRIkCgVlbnRyeRgBIAEoCzIVLmZpbGVzeXN0ZW0uRW50cnlJbmZvIh4KDk1ha2VEaXJSZXF1ZXN0EgwKBHBhdGgYASABKAkiNwoPTWFrZURpclJlc3BvbnNlEiQKBWVudHJ5GAEgASgLMhUuZmlsZXN5c3RlbS5FbnRyeUluZm8iHQoNUmVtb3ZlUmVxdWVzdBIMCgRwYXRoGAEgASgJIhAKDlJlbW92ZVJlc3BvbnNlIhsKC1N0YXRSZXF1ZXN0EgwKBHBhdGgYASABKAkiNAoMU3RhdFJlc3BvbnNlEiQKBWVudHJ5GAEgASgLMhUuZmlsZXN5c3RlbS5FbnRyeUluZm8iSwoJRW50cnlJbmZvEgwKBG5hbWUYASABKAkSIgoEdHlwZRgCIAEoDjIULmZpbGVzeXN0ZW0uRmlsZVR5cGUSDAoEcGF0aBgDIAEoCSIeCg5MaXN0RGlyUmVxdWVzdBIMCgRwYXRoGAEgASgJIjkKD0xpc3REaXJSZXNwb25zZRImCgdlbnRyaWVzGAEgAygLMhUuZmlsZXN5c3RlbS5FbnRyeUluZm8iHwoPV2F0Y2hEaXJSZXF1ZXN0EgwKBHBhdGgYASABKAkiRAoPRmlsZXN5c3RlbUV2ZW50EgwKBG5hbWUYASABKAkSIwoEdHlwZRgCIAEoDjIVLmZpbGVzeXN0ZW0uRXZlbnRUeXBlIuABChBXYXRjaERpclJlc3BvbnNlEjgKBXN0YXJ0GAEgASgLMicuZmlsZXN5c3RlbS5XYXRjaERpclJlc3BvbnNlLlN0YXJ0RXZlbnRIABIxCgpmaWxlc3lzdGVtGAIgASgLMhsuZmlsZXN5c3RlbS5GaWxlc3lzdGVtRXZlbnRIABI7CglrZWVwYWxpdmUYAyABKAsyJi5maWxlc3lzdGVtLldhdGNoRGlyUmVzcG9uc2UuS2VlcEFsaXZlSAAaDAoKU3RhcnRFdmVudBoLCglLZWVwQWxpdmVCBwoFZXZlbnQiJAoUQ3JlYXRlV2F0Y2hlclJlcXVlc3QSDAoEcGF0aBgBIAEoCSIrChVDcmVhdGVXYXRjaGVyUmVzcG9uc2USEgoKd2F0Y2hlcl9pZBgBIAEoCSItChdHZXRXYXRjaGVyRXZlbnRzUmVxdWVzdBISCgp3YXRjaGVyX2lkGAEgASgJIkcKGEdldFdhdGNoZXJFdmVudHNSZXNwb25zZRIrCgZldmVudHMYASADKAsyGy5maWxlc3lzdGVtLkZpbGVzeXN0ZW1FdmVudCIqChRSZW1vdmVXYXRjaGVyUmVxdWVzdBISCgp3YXRjaGVyX2lkGAEgASgJIhcKFVJlbW92ZVdhdGNoZXJSZXNwb25zZSpSCghGaWxlVHlwZRIZChVGSUxFX1RZUEVfVU5TUEVDSUZJRUQQABISCg5GSUxFX1RZUEVfRklMRRABEhcKE0ZJTEVfVFlQRV9ESVJFQ1RPUlkQAiqYAQoJRXZlbnRUeXBlEhoKFkVWRU5UX1RZUEVfVU5TUEVDSUZJRUQQABIVChFFVkVOVF9UWVBFX0NSRUFURRABEhQKEEVWRU5UX1RZUEVfV1JJVEUQAhIVChFFVkVOVF9UWVBFX1JFTU9WRRADEhUKEUVWRU5UX1RZUEVfUkVOQU1FEAQSFAoQRVZFTlRfVFlQRV9DSE1PRBAFMp8FCgpGaWxlc3lzdGVtEjkKBFN0YXQSFy5maWxlc3lzdGVtLlN0YXRSZXF1ZXN0GhguZmlsZXN5c3RlbS5TdGF0UmVzcG9uc2USQgoHTWFrZURpchIaLmZpbGVzeXN0ZW0uTWFrZURpclJlcXVlc3QaGy5maWxlc3lzdGVtLk1ha2VEaXJSZXNwb25zZRI5CgRNb3ZlEhcuZmlsZXN5c3RlbS5Nb3ZlUmVxdWVzdBoYLmZpbGVzeXN0ZW0uTW92ZVJlc3BvbnNlEkIKB0xpc3REaXISGi5maWxlc3lzdGVtLkxpc3REaXJSZXF1ZXN0GhsuZmlsZXN5c3RlbS5MaXN0RGlyUmVzcG9uc2USPwoGUmVtb3ZlEhkuZmlsZXN5c3RlbS5SZW1vdmVSZXF1ZXN0GhouZmlsZXN5c3RlbS5SZW1vdmVSZXNwb25zZRJHCghXYXRjaERpchIbLmZpbGVzeXN0ZW0uV2F0Y2hEaXJSZXF1ZXN0GhwuZmlsZXN5c3RlbS5XYXRjaERpclJlc3BvbnNlMAESVAoNQ3JlYXRlV2F0Y2hlchIgLmZpbGVzeXN0ZW0uQ3JlYXRlV2F0Y2hlclJlcXVlc3QaIS5maWxlc3lzdGVtLkNyZWF0ZVdhdGNoZXJSZXNwb25zZRJdChBHZXRXYXRjaGVyRXZlbnRzEiMuZmlsZXN5c3RlbS5HZXRXYXRjaGVyRXZlbnRzUmVxdWVzdBokLmZpbGVzeXN0ZW0uR2V0V2F0Y2hlckV2ZW50c1Jlc3BvbnNlElQKDVJlbW92ZVdhdGNoZXISIC5maWxlc3lzdGVtLlJlbW92ZVdhdGNoZXJSZXF1ZXN0GiEuZmlsZXN5c3RlbS5SZW1vdmVXYXRjaGVyUmVzcG9uc2VCaQoOY29tLmZpbGVzeXN0ZW1CD0ZpbGVzeXN0ZW1Qcm90b1ABogIDRlhYqgIKRmlsZXN5c3RlbcoCCkZpbGVzeXN0ZW3iAhZGaWxlc3lzdGVtXEdQQk1ldGFkYXRh6gIKRmlsZXN5c3RlbWIGcHJvdG8z");
fileDesc("ChtmaWxlc3lzdGVtL2ZpbGVzeXN0ZW0ucHJvdG8SCmZpbGVzeXN0ZW0iMgoLTW92ZVJlcXVlc3QSDgoGc291cmNlGAEgASgJEhMKC2Rlc3RpbmF0aW9uGAIgASgJIjQKDE1vdmVSZXNwb25zZRIkCgVlbnRyeRgBIAEoCzIVLmZpbGVzeXN0ZW0uRW50cnlJbmZvIh4KDk1ha2VEaXJSZXF1ZXN0EgwKBHBhdGgYASABKAkiNwoPTWFrZURpclJlc3BvbnNlEiQKBWVudHJ5GAEgASgLMhUuZmlsZXN5c3RlbS5FbnRyeUluZm8iHQoNUmVtb3ZlUmVxdWVzdBIMCgRwYXRoGAEgASgJIhAKDlJlbW92ZVJlc3BvbnNlIhsKC1N0YXRSZXF1ZXN0EgwKBHBhdGgYASABKAkiNAoMU3RhdFJlc3BvbnNlEiQKBWVudHJ5GAEgASgLMhUuZmlsZXN5c3RlbS5FbnRyeUluZm8iSwoJRW50cnlJbmZvEgwKBG5hbWUYASABKAkSIgoEdHlwZRgCIAEoDjIULmZpbGVzeXN0ZW0uRmlsZVR5cGUSDAoEcGF0aBgDIAEoCSIeCg5MaXN0RGlyUmVxdWVzdBIMCgRwYXRoGAEgASgJIjkKD0xpc3REaXJSZXNwb25zZRImCgdlbnRyaWVzGAEgAygLMhUuZmlsZXN5c3RlbS5FbnRyeUluZm8iMgoPV2F0Y2hEaXJSZXF1ZXN0EgwKBHBhdGgYASABKAkSEQoJcmVjdXJzaXZlGAIgASgIIkQKD0ZpbGVzeXN0ZW1FdmVudBIMCgRuYW1lGAEgASgJEiMKBHR5cGUYAiABKA4yFS5maWxlc3lzdGVtLkV2ZW50VHlwZSLgAQoQV2F0Y2hEaXJSZXNwb25zZRI4CgVzdGFydBgBIAEoCzInLmZpbGVzeXN0ZW0uV2F0Y2hEaXJSZXNwb25zZS5TdGFydEV2ZW50SAASMQoKZmlsZXN5c3RlbRgCIAEoCzIbLmZpbGVzeXN0ZW0uRmlsZXN5c3RlbUV2ZW50SAASOwoJa2VlcGFsaXZlGAMgASgLMiYuZmlsZXN5c3RlbS5XYXRjaERpclJlc3BvbnNlLktlZXBBbGl2ZUgAGgwKClN0YXJ0RXZlbnQaCwoJS2VlcEFsaXZlQgcKBWV2ZW50IjcKFENyZWF0ZVdhdGNoZXJSZXF1ZXN0EgwKBHBhdGgYASABKAkSEQoJcmVjdXJzaXZlGAIgASgIIisKFUNyZWF0ZVdhdGNoZXJSZXNwb25zZRISCgp3YXRjaGVyX2lkGAEgASgJIi0KF0dldFdhdGNoZXJFdmVudHNSZXF1ZXN0EhIKCndhdGNoZXJfaWQYASABKAkiRwoYR2V0V2F0Y2hlckV2ZW50c1Jlc3BvbnNlEisKBmV2ZW50cxgBIAMoCzIbLmZpbGVzeXN0ZW0uRmlsZXN5c3RlbUV2ZW50IioKFFJlbW92ZVdhdGNoZXJSZXF1ZXN0EhIKCndhdGNoZXJfaWQYASABKAkiFwoVUmVtb3ZlV2F0Y2hlclJlc3BvbnNlKlIKCEZpbGVUeXBlEhkKFUZJTEVfVFlQRV9VTlNQRUNJRklFRBAAEhIKDkZJTEVfVFlQRV9GSUxFEAESFwoTRklMRV9UWVBFX0RJUkVDVE9SWRACKpgBCglFdmVudFR5cGUSGgoWRVZFTlRfVFlQRV9VTlNQRUNJRklFRBAAEhUKEUVWRU5UX1RZUEVfQ1JFQVRFEAESFAoQRVZFTlRfVFlQRV9XUklURRACEhUKEUVWRU5UX1RZUEVfUkVNT1ZFEAMSFQoRRVZFTlRfVFlQRV9SRU5BTUUQBBIUChBFVkVOVF9UWVBFX0NITU9EEAUynwUKCkZpbGVzeXN0ZW0SOQoEU3RhdBIXLmZpbGVzeXN0ZW0uU3RhdFJlcXVlc3QaGC5maWxlc3lzdGVtLlN0YXRSZXNwb25zZRJCCgdNYWtlRGlyEhouZmlsZXN5c3RlbS5NYWtlRGlyUmVxdWVzdBobLmZpbGVzeXN0ZW0uTWFrZURpclJlc3BvbnNlEjkKBE1vdmUSFy5maWxlc3lzdGVtLk1vdmVSZXF1ZXN0GhguZmlsZXN5c3RlbS5Nb3ZlUmVzcG9uc2USQgoHTGlzdERpchIaLmZpbGVzeXN0ZW0uTGlzdERpclJlcXVlc3QaGy5maWxlc3lzdGVtLkxpc3REaXJSZXNwb25zZRI/CgZSZW1vdmUSGS5maWxlc3lzdGVtLlJlbW92ZVJlcXVlc3QaGi5maWxlc3lzdGVtLlJlbW92ZVJlc3BvbnNlEkcKCFdhdGNoRGlyEhsuZmlsZXN5c3RlbS5XYXRjaERpclJlcXVlc3QaHC5maWxlc3lzdGVtLldhdGNoRGlyUmVzcG9uc2UwARJUCg1DcmVhdGVXYXRjaGVyEiAuZmlsZXN5c3RlbS5DcmVhdGVXYXRjaGVyUmVxdWVzdBohLmZpbGVzeXN0ZW0uQ3JlYXRlV2F0Y2hlclJlc3BvbnNlEl0KEEdldFdhdGNoZXJFdmVudHMSIy5maWxlc3lzdGVtLkdldFdhdGNoZXJFdmVudHNSZXF1ZXN0GiQuZmlsZXN5c3RlbS5HZXRXYXRjaGVyRXZlbnRzUmVzcG9uc2USVAoNUmVtb3ZlV2F0Y2hlchIgLmZpbGVzeXN0ZW0uUmVtb3ZlV2F0Y2hlclJlcXVlc3QaIS5maWxlc3lzdGVtLlJlbW92ZVdhdGNoZXJSZXNwb25zZUJpCg5jb20uZmlsZXN5c3RlbUIPRmlsZXN5c3RlbVByb3RvUAGiAgNGWFiqAgpGaWxlc3lzdGVtygIKRmlsZXN5c3RlbeICFkZpbGVzeXN0ZW1cR1BCTWV0YWRhdGHqAgpGaWxlc3lzdGVtYgZwcm90bzM");

/**
* @generated from message filesystem.MoveRequest
Expand Down Expand Up @@ -218,6 +218,11 @@ export type WatchDirRequest = Message<"filesystem.WatchDirRequest"> & {
* @generated from field: string path = 1;
*/
path: string;

/**
* @generated from field: bool recursive = 2;
*/
recursive: boolean;
};

/**
Expand Down Expand Up @@ -318,6 +323,11 @@ export type CreateWatcherRequest = Message<"filesystem.CreateWatcherRequest"> &
* @generated from field: string path = 1;
*/
path: string;

/**
* @generated from field: bool recursive = 2;
*/
recursive: boolean;
};

/**
Expand Down
1 change: 1 addition & 0 deletions packages/js-sdk/src/envd/versions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const ENVD_VERSION_RECURSIVE_WATCH = '0.1.4'
21 changes: 20 additions & 1 deletion packages/js-sdk/src/sandbox/filesystem/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ import { FileType as FsFileType, Filesystem as FilesystemService } from '../../e

import { WatchHandle, FilesystemEvent } from './watchHandle'

import { compareVersions } from 'compare-versions'
import { TemplateError } from '../../errors'
import { ENVD_VERSION_RECURSIVE_WATCH } from '../../envd/versions'

/**
* Sandbox filesystem object information.
*/
Expand Down Expand Up @@ -90,6 +94,10 @@ export interface WatchOpts extends FilesystemRequestOpts {
* Callback to call when the watch operation stops.
*/
onExit?: (err?: Error) => void | Promise<void>
/**
* Watch the directory recursively
*/
recursive?: boolean
}

/**
Expand All @@ -99,6 +107,7 @@ export class Filesystem {
private readonly rpc: Client<typeof FilesystemService>

private readonly defaultWatchTimeout = 60_000 // 60 seconds
private readonly defaultWatchRecursive = false

constructor(
transport: Transport,
Expand Down Expand Up @@ -434,6 +443,13 @@ export class Filesystem {
onEvent: (event: FilesystemEvent) => void | Promise<void>,
opts?: WatchOpts
): Promise<WatchHandle> {
if (opts?.recursive && this.envdApi.version && compareVersions(this.envdApi.version, ENVD_VERSION_RECURSIVE_WATCH) < 0) {
throw new TemplateError(
'You need to update the template to use recursive watching. ' +
'You can do this by running `e2b template build` in the directory with the template.'
)
}

const requestTimeoutMs =
opts?.requestTimeoutMs ?? this.connectionConfig.requestTimeoutMs

Expand All @@ -446,7 +462,10 @@ export class Filesystem {
: undefined

const events = this.rpc.watchDir(
{ path },
{
path,
recursive: opts?.recursive ?? this.defaultWatchRecursive,
},
{
headers: {
...authenticationHeader(opts?.user),
Expand Down
81 changes: 79 additions & 2 deletions packages/js-sdk/tests/sandbox/files/watch.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect } from 'vitest'
import { expect, onTestFinished } from 'vitest'

import { sandboxTest } from '../../setup.js'
import { isDebug, sandboxTest } from '../../setup.js'
import { FilesystemEventType, NotFoundError, SandboxError } from '../../../src'

sandboxTest('watch directory changes', async ({ sandbox }) => {
Expand Down Expand Up @@ -31,6 +31,83 @@ sandboxTest('watch directory changes', async ({ sandbox }) => {
await handle.stop()
})

sandboxTest('watch recursive directory changes', async ({ sandbox }) => {
const dirname = 'test_recursive_watch_dir'
const nestedDirname = 'test_nested_watch_dir'
const filename = 'test_watch.txt'
const content = 'This file will be watched.'
const newContent = 'This file has been modified.'

await sandbox.files.makeDir(`${dirname}/${nestedDirname}`)
if (isDebug) {
onTestFinished(() => sandbox.files.remove(dirname))
}

await sandbox.files.write(`${dirname}/${nestedDirname}/${filename}`, content)

let trigger: () => void

const eventPromise = new Promise<void>((resolve) => {
trigger = resolve
})

const expectedFileName = `${nestedDirname}/${filename}`
const handle = await sandbox.files.watchDir(dirname, async (event) => {
if (event.type === FilesystemEventType.WRITE && event.name === expectedFileName) {
trigger()
}
}, {
recursive: true
})

await sandbox.files.write(`${dirname}/${nestedDirname}/${filename}`, newContent)

await eventPromise

await handle.stop()
})

sandboxTest('watch recursive directory after nested folder addition', async ({ sandbox }) => {
const dirname = 'test_recursive_watch_dir_add'
const nestedDirname = 'test_nested_watch_dir'
const filename = 'test_watch.txt'
const content = 'This file will be watched.'

await sandbox.files.makeDir(dirname)
if (isDebug) {
onTestFinished(() => sandbox.files.remove(dirname))
}

let triggerFile: () => void
let triggerFolder: () => void

const eventFilePromise = new Promise<void>((resolve) => {
triggerFile = resolve
})
const eventFolderPromise = new Promise<void>((resolve) => {
triggerFolder = resolve
})

const expectedFileName = `${nestedDirname}/${filename}`
const handle = await sandbox.files.watchDir(dirname, async (event) => {
if (event.type === FilesystemEventType.WRITE && event.name === expectedFileName) {
triggerFile()
} else if (event.type === FilesystemEventType.CREATE && event.name === nestedDirname) {
triggerFolder()
}
}, {
recursive: true
})

await sandbox.files.makeDir(`${dirname}/${nestedDirname}`)
await eventFolderPromise

await sandbox.files.write(`${dirname}/${nestedDirname}/${filename}`, content)
await eventFilePromise

await handle.stop()
})

sandboxTest('watch non-existing directory', async ({ sandbox }) => {
const dirname = 'non_existing_watch_dir'

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 4fb4470

Please sign in to comment.