diff --git a/apps/web/src/app/(docs)/docs/filesystem/watch/page.mdx b/apps/web/src/app/(docs)/docs/filesystem/watch/page.mdx
index 48bd3e346..00dc0c112 100644
--- a/apps/web/src/app/(docs)/docs/filesystem/watch/page.mdx
+++ b/apps/web/src/app/(docs)/docs/filesystem/watch/page.mdx
@@ -39,3 +39,52 @@ for event in events: # $HighlightLine
print(f"wrote to file {event.name}") # $HighlightLine
```
+
+
+## Recursive Watching
+
+You can enable recursive watching using the parameter `recursive`.
+
+
+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.
+
+
+
+```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}")
+```
+
diff --git a/packages/js-sdk/src/envd/api.ts b/packages/js-sdk/src/envd/api.ts
index 0ae3a6389..7f55bf7eb 100644
--- a/packages/js-sdk/src/envd/api.ts
+++ b/packages/js-sdk/src/envd/api.ts
@@ -95,12 +95,19 @@ export async function handleWatchDirStartEvent(
class EnvdApiClient {
readonly api: ReturnType>
+ readonly version: string | undefined
- constructor(config: Pick) {
+ constructor(
+ config: Pick,
+ metadata: {
+ version?: string
+ }
+ ) {
this.api = createClient({
baseUrl: config.apiUrl,
// keepalive: true, // TODO: Return keepalive
})
+ this.version = metadata.version
if (config.logger) {
this.api.use(createApiLogger(config.logger))
diff --git a/packages/js-sdk/src/envd/filesystem/filesystem_pb.ts b/packages/js-sdk/src/envd/filesystem/filesystem_pb.ts
index 10082f531..bf3763f36 100644
--- a/packages/js-sdk/src/envd/filesystem/filesystem_pb.ts
+++ b/packages/js-sdk/src/envd/filesystem/filesystem_pb.ts
@@ -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
@@ -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;
};
/**
@@ -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;
};
/**
diff --git a/packages/js-sdk/src/sandbox/filesystem/index.ts b/packages/js-sdk/src/sandbox/filesystem/index.ts
index f65003ae8..5536a2447 100644
--- a/packages/js-sdk/src/sandbox/filesystem/index.ts
+++ b/packages/js-sdk/src/sandbox/filesystem/index.ts
@@ -22,6 +22,9 @@ import { FileType as FsFileType, Filesystem as FilesystemService } from '../../e
import { WatchHandle, FilesystemEvent } from './watchHandle'
+import { compareVersions } from 'compare-versions'
+import { TemplateError } from '../../errors'
+
/**
* Sandbox filesystem object information.
*/
@@ -90,6 +93,10 @@ export interface WatchOpts extends FilesystemRequestOpts {
* Callback to call when the watch operation stops.
*/
onExit?: (err?: Error) => void | Promise
+ /**
+ * Watch the directory recursively
+ */
+ recursive?: boolean
}
/**
@@ -99,6 +106,7 @@ export class Filesystem {
private readonly rpc: Client
private readonly defaultWatchTimeout = 60_000 // 60 seconds
+ private readonly defaultWatchRecursive = false
constructor(
transport: Transport,
@@ -434,6 +442,13 @@ export class Filesystem {
onEvent: (event: FilesystemEvent) => void | Promise,
opts?: WatchOpts
): Promise {
+ if (opts?.recursive && this.envdApi.version && compareVersions(this.envdApi.version, '0.1.3') < 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
@@ -446,7 +461,10 @@ export class Filesystem {
: undefined
const events = this.rpc.watchDir(
- { path },
+ {
+ path,
+ recursive: opts?.recursive ?? this.defaultWatchRecursive,
+ },
{
headers: {
...authenticationHeader(opts?.user),
diff --git a/packages/js-sdk/src/sandbox/index.ts b/packages/js-sdk/src/sandbox/index.ts
index 37edbcae6..46239ff52 100644
--- a/packages/js-sdk/src/sandbox/index.ts
+++ b/packages/js-sdk/src/sandbox/index.ts
@@ -99,6 +99,7 @@ export class Sandbox extends SandboxApi {
constructor(
opts: Omit & {
sandboxId: string
+ envdVersion?: string
}
) {
super()
@@ -115,10 +116,15 @@ export class Sandbox extends SandboxApi {
interceptors: opts?.logger ? [createRpcLogger(opts.logger)] : undefined,
})
- this.envdApi = new EnvdApiClient({
- apiUrl: this.envdApiUrl,
- logger: opts?.logger,
- })
+ this.envdApi = new EnvdApiClient(
+ {
+ apiUrl: this.envdApiUrl,
+ logger: opts?.logger,
+ },
+ {
+ version: opts?.envdVersion
+ }
+ )
this.files = new Filesystem(
rpcTransport,
this.envdApi,
@@ -177,15 +183,15 @@ export class Sandbox extends SandboxApi {
const config = new ConnectionConfig(sandboxOpts)
- const sandboxId = config.debug
- ? 'debug_sandbox_id'
+ const sandbox = config.debug
+ ? { sandboxId: 'debug_sandbox_id' }
: await this.createSandbox(
template,
sandboxOpts?.timeoutMs ?? this.defaultSandboxTimeoutMs,
sandboxOpts
)
- const sbx = new this({ sandboxId, ...config }) as InstanceType
+ const sbx = new this({ ...sandbox, ...config }) as InstanceType
return sbx
}
diff --git a/packages/js-sdk/src/sandbox/sandboxApi.ts b/packages/js-sdk/src/sandbox/sandboxApi.ts
index 1cf997f4d..edddc15e2 100644
--- a/packages/js-sdk/src/sandbox/sandboxApi.ts
+++ b/packages/js-sdk/src/sandbox/sandboxApi.ts
@@ -159,7 +159,10 @@ export class SandboxApi {
metadata?: Record
envs?: Record
}
- ): Promise {
+ ): Promise<{
+ sandboxId: string
+ envdVersion: string
+ }> {
const config = new ConnectionConfig(opts)
const client = new ApiClient(config)
@@ -191,10 +194,13 @@ export class SandboxApi {
'You can do this by running `e2b template build` in the directory with the template.'
)
}
- return this.getSandboxId({
- sandboxId: res.data!.sandboxID,
- clientId: res.data!.clientID,
- })
+ return {
+ sandboxId: this.getSandboxId({
+ sandboxId: res.data!.sandboxID,
+ clientId: res.data!.clientID,
+ }),
+ envdVersion: res.data!.envdVersion
+ }
}
private static timeoutToSeconds(timeout: number): number {
diff --git a/packages/js-sdk/tests/sandbox/files/watch.test.ts b/packages/js-sdk/tests/sandbox/files/watch.test.ts
index 4cb9251f9..e585405cb 100644
--- a/packages/js-sdk/tests/sandbox/files/watch.test.ts
+++ b/packages/js-sdk/tests/sandbox/files/watch.test.ts
@@ -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 }) => {
@@ -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((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((resolve) => {
+ triggerFile = resolve
+ })
+ const eventFolderPromise = new Promise((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'
diff --git a/packages/python-sdk/e2b/api/client/api/default/get_health.py b/packages/python-sdk/e2b/api/client/api/default/get_health.py
index 339a6905f..c295ec3c2 100644
--- a/packages/python-sdk/e2b/api/client/api/default/get_health.py
+++ b/packages/python-sdk/e2b/api/client/api/default/get_health.py
@@ -17,7 +17,9 @@ def _get_kwargs() -> Dict[str, Any]:
return _kwargs
-def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]:
+def _parse_response(
+ *, client: Union[AuthenticatedClient, Client], response: httpx.Response
+) -> Optional[Any]:
if response.status_code == HTTPStatus.OK:
return None
if response.status_code == HTTPStatus.UNAUTHORIZED:
@@ -28,7 +30,9 @@ def _parse_response(*, client: Union[AuthenticatedClient, Client], response: htt
return None
-def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]:
+def _build_response(
+ *, client: Union[AuthenticatedClient, Client], response: httpx.Response
+) -> Response[Any]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
diff --git a/packages/python-sdk/e2b/api/client/api/sandboxes/delete_sandboxes_sandbox_id.py b/packages/python-sdk/e2b/api/client/api/sandboxes/delete_sandboxes_sandbox_id.py
index fa9f37750..974f2d687 100644
--- a/packages/python-sdk/e2b/api/client/api/sandboxes/delete_sandboxes_sandbox_id.py
+++ b/packages/python-sdk/e2b/api/client/api/sandboxes/delete_sandboxes_sandbox_id.py
@@ -19,7 +19,9 @@ def _get_kwargs(
return _kwargs
-def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]:
+def _parse_response(
+ *, client: Union[AuthenticatedClient, Client], response: httpx.Response
+) -> Optional[Any]:
if response.status_code == HTTPStatus.NO_CONTENT:
return None
if response.status_code == HTTPStatus.NOT_FOUND:
@@ -34,7 +36,9 @@ def _parse_response(*, client: Union[AuthenticatedClient, Client], response: htt
return None
-def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]:
+def _build_response(
+ *, client: Union[AuthenticatedClient, Client], response: httpx.Response
+) -> Response[Any]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
diff --git a/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_refreshes.py b/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_refreshes.py
index 163bcf177..39dfc678a 100644
--- a/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_refreshes.py
+++ b/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_refreshes.py
@@ -5,7 +5,9 @@
from ... import errors
from ...client import AuthenticatedClient, Client
-from ...models.post_sandboxes_sandbox_id_refreshes_body import PostSandboxesSandboxIDRefreshesBody
+from ...models.post_sandboxes_sandbox_id_refreshes_body import (
+ PostSandboxesSandboxIDRefreshesBody,
+)
from ...types import Response
@@ -30,7 +32,9 @@ def _get_kwargs(
return _kwargs
-def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]:
+def _parse_response(
+ *, client: Union[AuthenticatedClient, Client], response: httpx.Response
+) -> Optional[Any]:
if response.status_code == HTTPStatus.NO_CONTENT:
return None
if response.status_code == HTTPStatus.UNAUTHORIZED:
@@ -43,7 +47,9 @@ def _parse_response(*, client: Union[AuthenticatedClient, Client], response: htt
return None
-def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]:
+def _build_response(
+ *, client: Union[AuthenticatedClient, Client], response: httpx.Response
+) -> Response[Any]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
diff --git a/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_timeout.py b/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_timeout.py
index 148d97c87..615963abf 100644
--- a/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_timeout.py
+++ b/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_timeout.py
@@ -5,7 +5,9 @@
from ... import errors
from ...client import AuthenticatedClient, Client
-from ...models.post_sandboxes_sandbox_id_timeout_body import PostSandboxesSandboxIDTimeoutBody
+from ...models.post_sandboxes_sandbox_id_timeout_body import (
+ PostSandboxesSandboxIDTimeoutBody,
+)
from ...types import Response
@@ -30,7 +32,9 @@ def _get_kwargs(
return _kwargs
-def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]:
+def _parse_response(
+ *, client: Union[AuthenticatedClient, Client], response: httpx.Response
+) -> Optional[Any]:
if response.status_code == HTTPStatus.NO_CONTENT:
return None
if response.status_code == HTTPStatus.UNAUTHORIZED:
@@ -45,7 +49,9 @@ def _parse_response(*, client: Union[AuthenticatedClient, Client], response: htt
return None
-def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]:
+def _build_response(
+ *, client: Union[AuthenticatedClient, Client], response: httpx.Response
+) -> Response[Any]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
diff --git a/packages/python-sdk/e2b/api/client/api/templates/delete_templates_template_id.py b/packages/python-sdk/e2b/api/client/api/templates/delete_templates_template_id.py
index d493420f3..b7c162b27 100644
--- a/packages/python-sdk/e2b/api/client/api/templates/delete_templates_template_id.py
+++ b/packages/python-sdk/e2b/api/client/api/templates/delete_templates_template_id.py
@@ -19,7 +19,9 @@ def _get_kwargs(
return _kwargs
-def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]:
+def _parse_response(
+ *, client: Union[AuthenticatedClient, Client], response: httpx.Response
+) -> Optional[Any]:
if response.status_code == HTTPStatus.NO_CONTENT:
return None
if response.status_code == HTTPStatus.UNAUTHORIZED:
@@ -32,7 +34,9 @@ def _parse_response(*, client: Union[AuthenticatedClient, Client], response: htt
return None
-def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]:
+def _build_response(
+ *, client: Union[AuthenticatedClient, Client], response: httpx.Response
+) -> Response[Any]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
diff --git a/packages/python-sdk/e2b/api/client/api/templates/post_templates_template_id_builds_build_id.py b/packages/python-sdk/e2b/api/client/api/templates/post_templates_template_id_builds_build_id.py
index 67e21c739..754229875 100644
--- a/packages/python-sdk/e2b/api/client/api/templates/post_templates_template_id_builds_build_id.py
+++ b/packages/python-sdk/e2b/api/client/api/templates/post_templates_template_id_builds_build_id.py
@@ -20,7 +20,9 @@ def _get_kwargs(
return _kwargs
-def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]:
+def _parse_response(
+ *, client: Union[AuthenticatedClient, Client], response: httpx.Response
+) -> Optional[Any]:
if response.status_code == HTTPStatus.ACCEPTED:
return None
if response.status_code == HTTPStatus.UNAUTHORIZED:
@@ -33,7 +35,9 @@ def _parse_response(*, client: Union[AuthenticatedClient, Client], response: htt
return None
-def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]:
+def _build_response(
+ *, client: Union[AuthenticatedClient, Client], response: httpx.Response
+) -> Response[Any]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
diff --git a/packages/python-sdk/e2b/api/client/client.py b/packages/python-sdk/e2b/api/client/client.py
index 63a2493b9..38b07d057 100644
--- a/packages/python-sdk/e2b/api/client/client.py
+++ b/packages/python-sdk/e2b/api/client/client.py
@@ -38,9 +38,15 @@ class Client:
_base_url: str = field(alias="base_url")
_cookies: Dict[str, str] = field(factory=dict, kw_only=True, alias="cookies")
_headers: Dict[str, str] = field(factory=dict, kw_only=True, alias="headers")
- _timeout: Optional[httpx.Timeout] = field(default=None, kw_only=True, alias="timeout")
- _verify_ssl: Union[str, bool, ssl.SSLContext] = field(default=True, kw_only=True, alias="verify_ssl")
- _follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects")
+ _timeout: Optional[httpx.Timeout] = field(
+ default=None, kw_only=True, alias="timeout"
+ )
+ _verify_ssl: Union[str, bool, ssl.SSLContext] = field(
+ default=True, kw_only=True, alias="verify_ssl"
+ )
+ _follow_redirects: bool = field(
+ default=False, kw_only=True, alias="follow_redirects"
+ )
_httpx_args: Dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args")
_client: Optional[httpx.Client] = field(default=None, init=False)
_async_client: Optional[httpx.AsyncClient] = field(default=None, init=False)
@@ -168,9 +174,15 @@ class AuthenticatedClient:
_base_url: str = field(alias="base_url")
_cookies: Dict[str, str] = field(factory=dict, kw_only=True, alias="cookies")
_headers: Dict[str, str] = field(factory=dict, kw_only=True, alias="headers")
- _timeout: Optional[httpx.Timeout] = field(default=None, kw_only=True, alias="timeout")
- _verify_ssl: Union[str, bool, ssl.SSLContext] = field(default=True, kw_only=True, alias="verify_ssl")
- _follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects")
+ _timeout: Optional[httpx.Timeout] = field(
+ default=None, kw_only=True, alias="timeout"
+ )
+ _verify_ssl: Union[str, bool, ssl.SSLContext] = field(
+ default=True, kw_only=True, alias="verify_ssl"
+ )
+ _follow_redirects: bool = field(
+ default=False, kw_only=True, alias="follow_redirects"
+ )
_httpx_args: Dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args")
_client: Optional[httpx.Client] = field(default=None, init=False)
_async_client: Optional[httpx.AsyncClient] = field(default=None, init=False)
@@ -214,7 +226,9 @@ def set_httpx_client(self, client: httpx.Client) -> "AuthenticatedClient":
def get_httpx_client(self) -> httpx.Client:
"""Get the underlying httpx.Client, constructing a new one if not previously set"""
if self._client is None:
- self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token
+ self._headers[self.auth_header_name] = (
+ f"{self.prefix} {self.token}" if self.prefix else self.token
+ )
self._client = httpx.Client(
base_url=self._base_url,
cookies=self._cookies,
@@ -235,7 +249,9 @@ def __exit__(self, *args: Any, **kwargs: Any) -> None:
"""Exit a context manager for internal httpx.Client (see httpx docs)"""
self.get_httpx_client().__exit__(*args, **kwargs)
- def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> "AuthenticatedClient":
+ def set_async_httpx_client(
+ self, async_client: httpx.AsyncClient
+ ) -> "AuthenticatedClient":
"""Manually the underlying httpx.AsyncClient
**NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
@@ -246,7 +262,9 @@ def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> "Authentica
def get_async_httpx_client(self) -> httpx.AsyncClient:
"""Get the underlying httpx.AsyncClient, constructing a new one if not previously set"""
if self._async_client is None:
- self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token
+ self._headers[self.auth_header_name] = (
+ f"{self.prefix} {self.token}" if self.prefix else self.token
+ )
self._async_client = httpx.AsyncClient(
base_url=self._base_url,
cookies=self._cookies,
diff --git a/packages/python-sdk/e2b/api/client/models/__init__.py b/packages/python-sdk/e2b/api/client/models/__init__.py
index d55b958c2..85275a6de 100644
--- a/packages/python-sdk/e2b/api/client/models/__init__.py
+++ b/packages/python-sdk/e2b/api/client/models/__init__.py
@@ -2,7 +2,9 @@
from .error import Error
from .new_sandbox import NewSandbox
-from .post_sandboxes_sandbox_id_refreshes_body import PostSandboxesSandboxIDRefreshesBody
+from .post_sandboxes_sandbox_id_refreshes_body import (
+ PostSandboxesSandboxIDRefreshesBody,
+)
from .post_sandboxes_sandbox_id_timeout_body import PostSandboxesSandboxIDTimeoutBody
from .running_sandbox import RunningSandbox
from .sandbox import Sandbox
diff --git a/packages/python-sdk/e2b/envd/filesystem/filesystem_pb2.py b/packages/python-sdk/e2b/envd/filesystem/filesystem_pb2.py
index 27c58dac8..626367ae3 100644
--- a/packages/python-sdk/e2b/envd/filesystem/filesystem_pb2.py
+++ b/packages/python-sdk/e2b/envd/filesystem/filesystem_pb2.py
@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
# source: filesystem/filesystem.proto
-# Protobuf Python Version: 5.27.3
+# Protobuf Python Version: 5.28.3
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
@@ -14,7 +15,7 @@
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(
- b'\n\x1b\x66ilesystem/filesystem.proto\x12\nfilesystem"G\n\x0bMoveRequest\x12\x16\n\x06source\x18\x01 \x01(\tR\x06source\x12 \n\x0b\x64\x65stination\x18\x02 \x01(\tR\x0b\x64\x65stination";\n\x0cMoveResponse\x12+\n\x05\x65ntry\x18\x01 \x01(\x0b\x32\x15.filesystem.EntryInfoR\x05\x65ntry"$\n\x0eMakeDirRequest\x12\x12\n\x04path\x18\x01 \x01(\tR\x04path">\n\x0fMakeDirResponse\x12+\n\x05\x65ntry\x18\x01 \x01(\x0b\x32\x15.filesystem.EntryInfoR\x05\x65ntry"#\n\rRemoveRequest\x12\x12\n\x04path\x18\x01 \x01(\tR\x04path"\x10\n\x0eRemoveResponse"!\n\x0bStatRequest\x12\x12\n\x04path\x18\x01 \x01(\tR\x04path";\n\x0cStatResponse\x12+\n\x05\x65ntry\x18\x01 \x01(\x0b\x32\x15.filesystem.EntryInfoR\x05\x65ntry"]\n\tEntryInfo\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12(\n\x04type\x18\x02 \x01(\x0e\x32\x14.filesystem.FileTypeR\x04type\x12\x12\n\x04path\x18\x03 \x01(\tR\x04path"$\n\x0eListDirRequest\x12\x12\n\x04path\x18\x01 \x01(\tR\x04path"B\n\x0fListDirResponse\x12/\n\x07\x65ntries\x18\x01 \x03(\x0b\x32\x15.filesystem.EntryInfoR\x07\x65ntries"%\n\x0fWatchDirRequest\x12\x12\n\x04path\x18\x01 \x01(\tR\x04path"P\n\x0f\x46ilesystemEvent\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12)\n\x04type\x18\x02 \x01(\x0e\x32\x15.filesystem.EventTypeR\x04type"\xfe\x01\n\x10WatchDirResponse\x12?\n\x05start\x18\x01 \x01(\x0b\x32\'.filesystem.WatchDirResponse.StartEventH\x00R\x05start\x12=\n\nfilesystem\x18\x02 \x01(\x0b\x32\x1b.filesystem.FilesystemEventH\x00R\nfilesystem\x12\x46\n\tkeepalive\x18\x03 \x01(\x0b\x32&.filesystem.WatchDirResponse.KeepAliveH\x00R\tkeepalive\x1a\x0c\n\nStartEvent\x1a\x0b\n\tKeepAliveB\x07\n\x05\x65vent"*\n\x14\x43reateWatcherRequest\x12\x12\n\x04path\x18\x01 \x01(\tR\x04path"6\n\x15\x43reateWatcherResponse\x12\x1d\n\nwatcher_id\x18\x01 \x01(\tR\twatcherId"8\n\x17GetWatcherEventsRequest\x12\x1d\n\nwatcher_id\x18\x01 \x01(\tR\twatcherId"O\n\x18GetWatcherEventsResponse\x12\x33\n\x06\x65vents\x18\x01 \x03(\x0b\x32\x1b.filesystem.FilesystemEventR\x06\x65vents"5\n\x14RemoveWatcherRequest\x12\x1d\n\nwatcher_id\x18\x01 \x01(\tR\twatcherId"\x17\n\x15RemoveWatcherResponse*R\n\x08\x46ileType\x12\x19\n\x15\x46ILE_TYPE_UNSPECIFIED\x10\x00\x12\x12\n\x0e\x46ILE_TYPE_FILE\x10\x01\x12\x17\n\x13\x46ILE_TYPE_DIRECTORY\x10\x02*\x98\x01\n\tEventType\x12\x1a\n\x16\x45VENT_TYPE_UNSPECIFIED\x10\x00\x12\x15\n\x11\x45VENT_TYPE_CREATE\x10\x01\x12\x14\n\x10\x45VENT_TYPE_WRITE\x10\x02\x12\x15\n\x11\x45VENT_TYPE_REMOVE\x10\x03\x12\x15\n\x11\x45VENT_TYPE_RENAME\x10\x04\x12\x14\n\x10\x45VENT_TYPE_CHMOD\x10\x05\x32\x9f\x05\n\nFilesystem\x12\x39\n\x04Stat\x12\x17.filesystem.StatRequest\x1a\x18.filesystem.StatResponse\x12\x42\n\x07MakeDir\x12\x1a.filesystem.MakeDirRequest\x1a\x1b.filesystem.MakeDirResponse\x12\x39\n\x04Move\x12\x17.filesystem.MoveRequest\x1a\x18.filesystem.MoveResponse\x12\x42\n\x07ListDir\x12\x1a.filesystem.ListDirRequest\x1a\x1b.filesystem.ListDirResponse\x12?\n\x06Remove\x12\x19.filesystem.RemoveRequest\x1a\x1a.filesystem.RemoveResponse\x12G\n\x08WatchDir\x12\x1b.filesystem.WatchDirRequest\x1a\x1c.filesystem.WatchDirResponse0\x01\x12T\n\rCreateWatcher\x12 .filesystem.CreateWatcherRequest\x1a!.filesystem.CreateWatcherResponse\x12]\n\x10GetWatcherEvents\x12#.filesystem.GetWatcherEventsRequest\x1a$.filesystem.GetWatcherEventsResponse\x12T\n\rRemoveWatcher\x12 .filesystem.RemoveWatcherRequest\x1a!.filesystem.RemoveWatcherResponseBi\n\x0e\x63om.filesystemB\x0f\x46ilesystemProtoP\x01\xa2\x02\x03\x46XX\xaa\x02\nFilesystem\xca\x02\nFilesystem\xe2\x02\x16\x46ilesystem\\GPBMetadata\xea\x02\nFilesystemb\x06proto3'
+ b'\n\x1b\x66ilesystem/filesystem.proto\x12\nfilesystem"G\n\x0bMoveRequest\x12\x16\n\x06source\x18\x01 \x01(\tR\x06source\x12 \n\x0b\x64\x65stination\x18\x02 \x01(\tR\x0b\x64\x65stination";\n\x0cMoveResponse\x12+\n\x05\x65ntry\x18\x01 \x01(\x0b\x32\x15.filesystem.EntryInfoR\x05\x65ntry"$\n\x0eMakeDirRequest\x12\x12\n\x04path\x18\x01 \x01(\tR\x04path">\n\x0fMakeDirResponse\x12+\n\x05\x65ntry\x18\x01 \x01(\x0b\x32\x15.filesystem.EntryInfoR\x05\x65ntry"#\n\rRemoveRequest\x12\x12\n\x04path\x18\x01 \x01(\tR\x04path"\x10\n\x0eRemoveResponse"!\n\x0bStatRequest\x12\x12\n\x04path\x18\x01 \x01(\tR\x04path";\n\x0cStatResponse\x12+\n\x05\x65ntry\x18\x01 \x01(\x0b\x32\x15.filesystem.EntryInfoR\x05\x65ntry"]\n\tEntryInfo\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12(\n\x04type\x18\x02 \x01(\x0e\x32\x14.filesystem.FileTypeR\x04type\x12\x12\n\x04path\x18\x03 \x01(\tR\x04path"$\n\x0eListDirRequest\x12\x12\n\x04path\x18\x01 \x01(\tR\x04path"B\n\x0fListDirResponse\x12/\n\x07\x65ntries\x18\x01 \x03(\x0b\x32\x15.filesystem.EntryInfoR\x07\x65ntries"C\n\x0fWatchDirRequest\x12\x12\n\x04path\x18\x01 \x01(\tR\x04path\x12\x1c\n\trecursive\x18\x02 \x01(\x08R\trecursive"P\n\x0f\x46ilesystemEvent\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12)\n\x04type\x18\x02 \x01(\x0e\x32\x15.filesystem.EventTypeR\x04type"\xfe\x01\n\x10WatchDirResponse\x12?\n\x05start\x18\x01 \x01(\x0b\x32\'.filesystem.WatchDirResponse.StartEventH\x00R\x05start\x12=\n\nfilesystem\x18\x02 \x01(\x0b\x32\x1b.filesystem.FilesystemEventH\x00R\nfilesystem\x12\x46\n\tkeepalive\x18\x03 \x01(\x0b\x32&.filesystem.WatchDirResponse.KeepAliveH\x00R\tkeepalive\x1a\x0c\n\nStartEvent\x1a\x0b\n\tKeepAliveB\x07\n\x05\x65vent"H\n\x14\x43reateWatcherRequest\x12\x12\n\x04path\x18\x01 \x01(\tR\x04path\x12\x1c\n\trecursive\x18\x02 \x01(\x08R\trecursive"6\n\x15\x43reateWatcherResponse\x12\x1d\n\nwatcher_id\x18\x01 \x01(\tR\twatcherId"8\n\x17GetWatcherEventsRequest\x12\x1d\n\nwatcher_id\x18\x01 \x01(\tR\twatcherId"O\n\x18GetWatcherEventsResponse\x12\x33\n\x06\x65vents\x18\x01 \x03(\x0b\x32\x1b.filesystem.FilesystemEventR\x06\x65vents"5\n\x14RemoveWatcherRequest\x12\x1d\n\nwatcher_id\x18\x01 \x01(\tR\twatcherId"\x17\n\x15RemoveWatcherResponse*R\n\x08\x46ileType\x12\x19\n\x15\x46ILE_TYPE_UNSPECIFIED\x10\x00\x12\x12\n\x0e\x46ILE_TYPE_FILE\x10\x01\x12\x17\n\x13\x46ILE_TYPE_DIRECTORY\x10\x02*\x98\x01\n\tEventType\x12\x1a\n\x16\x45VENT_TYPE_UNSPECIFIED\x10\x00\x12\x15\n\x11\x45VENT_TYPE_CREATE\x10\x01\x12\x14\n\x10\x45VENT_TYPE_WRITE\x10\x02\x12\x15\n\x11\x45VENT_TYPE_REMOVE\x10\x03\x12\x15\n\x11\x45VENT_TYPE_RENAME\x10\x04\x12\x14\n\x10\x45VENT_TYPE_CHMOD\x10\x05\x32\x9f\x05\n\nFilesystem\x12\x39\n\x04Stat\x12\x17.filesystem.StatRequest\x1a\x18.filesystem.StatResponse\x12\x42\n\x07MakeDir\x12\x1a.filesystem.MakeDirRequest\x1a\x1b.filesystem.MakeDirResponse\x12\x39\n\x04Move\x12\x17.filesystem.MoveRequest\x1a\x18.filesystem.MoveResponse\x12\x42\n\x07ListDir\x12\x1a.filesystem.ListDirRequest\x1a\x1b.filesystem.ListDirResponse\x12?\n\x06Remove\x12\x19.filesystem.RemoveRequest\x1a\x1a.filesystem.RemoveResponse\x12G\n\x08WatchDir\x12\x1b.filesystem.WatchDirRequest\x1a\x1c.filesystem.WatchDirResponse0\x01\x12T\n\rCreateWatcher\x12 .filesystem.CreateWatcherRequest\x1a!.filesystem.CreateWatcherResponse\x12]\n\x10GetWatcherEvents\x12#.filesystem.GetWatcherEventsRequest\x1a$.filesystem.GetWatcherEventsResponse\x12T\n\rRemoveWatcher\x12 .filesystem.RemoveWatcherRequest\x1a!.filesystem.RemoveWatcherResponseBi\n\x0e\x63om.filesystemB\x0f\x46ilesystemProtoP\x01\xa2\x02\x03\x46XX\xaa\x02\nFilesystem\xca\x02\nFilesystem\xe2\x02\x16\x46ilesystem\\GPBMetadata\xea\x02\nFilesystemb\x06proto3'
)
_globals = globals()
@@ -24,13 +25,13 @@
)
if not _descriptor._USE_C_DESCRIPTORS:
_globals["DESCRIPTOR"]._loaded_options = None
- _globals["DESCRIPTOR"]._serialized_options = (
- b"\n\016com.filesystemB\017FilesystemProtoP\001\242\002\003FXX\252\002\nFilesystem\312\002\nFilesystem\342\002\026Filesystem\\GPBMetadata\352\002\nFilesystem"
- )
- _globals["_FILETYPE"]._serialized_start = 1328
- _globals["_FILETYPE"]._serialized_end = 1410
- _globals["_EVENTTYPE"]._serialized_start = 1413
- _globals["_EVENTTYPE"]._serialized_end = 1565
+ _globals[
+ "DESCRIPTOR"
+ ]._serialized_options = b"\n\016com.filesystemB\017FilesystemProtoP\001\242\002\003FXX\252\002\nFilesystem\312\002\nFilesystem\342\002\026Filesystem\\GPBMetadata\352\002\nFilesystem"
+ _globals["_FILETYPE"]._serialized_start = 1388
+ _globals["_FILETYPE"]._serialized_end = 1470
+ _globals["_EVENTTYPE"]._serialized_start = 1473
+ _globals["_EVENTTYPE"]._serialized_end = 1625
_globals["_MOVEREQUEST"]._serialized_start = 43
_globals["_MOVEREQUEST"]._serialized_end = 114
_globals["_MOVERESPONSE"]._serialized_start = 116
@@ -54,27 +55,27 @@
_globals["_LISTDIRRESPONSE"]._serialized_start = 563
_globals["_LISTDIRRESPONSE"]._serialized_end = 629
_globals["_WATCHDIRREQUEST"]._serialized_start = 631
- _globals["_WATCHDIRREQUEST"]._serialized_end = 668
- _globals["_FILESYSTEMEVENT"]._serialized_start = 670
- _globals["_FILESYSTEMEVENT"]._serialized_end = 750
- _globals["_WATCHDIRRESPONSE"]._serialized_start = 753
- _globals["_WATCHDIRRESPONSE"]._serialized_end = 1007
- _globals["_WATCHDIRRESPONSE_STARTEVENT"]._serialized_start = 973
- _globals["_WATCHDIRRESPONSE_STARTEVENT"]._serialized_end = 985
- _globals["_WATCHDIRRESPONSE_KEEPALIVE"]._serialized_start = 987
- _globals["_WATCHDIRRESPONSE_KEEPALIVE"]._serialized_end = 998
- _globals["_CREATEWATCHERREQUEST"]._serialized_start = 1009
- _globals["_CREATEWATCHERREQUEST"]._serialized_end = 1051
- _globals["_CREATEWATCHERRESPONSE"]._serialized_start = 1053
- _globals["_CREATEWATCHERRESPONSE"]._serialized_end = 1107
- _globals["_GETWATCHEREVENTSREQUEST"]._serialized_start = 1109
- _globals["_GETWATCHEREVENTSREQUEST"]._serialized_end = 1165
- _globals["_GETWATCHEREVENTSRESPONSE"]._serialized_start = 1167
- _globals["_GETWATCHEREVENTSRESPONSE"]._serialized_end = 1246
- _globals["_REMOVEWATCHERREQUEST"]._serialized_start = 1248
- _globals["_REMOVEWATCHERREQUEST"]._serialized_end = 1301
- _globals["_REMOVEWATCHERRESPONSE"]._serialized_start = 1303
- _globals["_REMOVEWATCHERRESPONSE"]._serialized_end = 1326
- _globals["_FILESYSTEM"]._serialized_start = 1568
- _globals["_FILESYSTEM"]._serialized_end = 2239
+ _globals["_WATCHDIRREQUEST"]._serialized_end = 698
+ _globals["_FILESYSTEMEVENT"]._serialized_start = 700
+ _globals["_FILESYSTEMEVENT"]._serialized_end = 780
+ _globals["_WATCHDIRRESPONSE"]._serialized_start = 783
+ _globals["_WATCHDIRRESPONSE"]._serialized_end = 1037
+ _globals["_WATCHDIRRESPONSE_STARTEVENT"]._serialized_start = 1003
+ _globals["_WATCHDIRRESPONSE_STARTEVENT"]._serialized_end = 1015
+ _globals["_WATCHDIRRESPONSE_KEEPALIVE"]._serialized_start = 1017
+ _globals["_WATCHDIRRESPONSE_KEEPALIVE"]._serialized_end = 1028
+ _globals["_CREATEWATCHERREQUEST"]._serialized_start = 1039
+ _globals["_CREATEWATCHERREQUEST"]._serialized_end = 1111
+ _globals["_CREATEWATCHERRESPONSE"]._serialized_start = 1113
+ _globals["_CREATEWATCHERRESPONSE"]._serialized_end = 1167
+ _globals["_GETWATCHEREVENTSREQUEST"]._serialized_start = 1169
+ _globals["_GETWATCHEREVENTSREQUEST"]._serialized_end = 1225
+ _globals["_GETWATCHEREVENTSRESPONSE"]._serialized_start = 1227
+ _globals["_GETWATCHEREVENTSRESPONSE"]._serialized_end = 1306
+ _globals["_REMOVEWATCHERREQUEST"]._serialized_start = 1308
+ _globals["_REMOVEWATCHERREQUEST"]._serialized_end = 1361
+ _globals["_REMOVEWATCHERRESPONSE"]._serialized_start = 1363
+ _globals["_REMOVEWATCHERRESPONSE"]._serialized_end = 1386
+ _globals["_FILESYSTEM"]._serialized_start = 1628
+ _globals["_FILESYSTEM"]._serialized_end = 2299
# @@protoc_insertion_point(module_scope)
diff --git a/packages/python-sdk/e2b/envd/filesystem/filesystem_pb2.pyi b/packages/python-sdk/e2b/envd/filesystem/filesystem_pb2.pyi
index a2208c060..c031b13ed 100644
--- a/packages/python-sdk/e2b/envd/filesystem/filesystem_pb2.pyi
+++ b/packages/python-sdk/e2b/envd/filesystem/filesystem_pb2.pyi
@@ -117,10 +117,12 @@ class ListDirResponse(_message.Message):
) -> None: ...
class WatchDirRequest(_message.Message):
- __slots__ = ("path",)
+ __slots__ = ("path", "recursive")
PATH_FIELD_NUMBER: _ClassVar[int]
+ RECURSIVE_FIELD_NUMBER: _ClassVar[int]
path: str
- def __init__(self, path: _Optional[str] = ...) -> None: ...
+ recursive: bool
+ def __init__(self, path: _Optional[str] = ..., recursive: bool = ...) -> None: ...
class FilesystemEvent(_message.Message):
__slots__ = ("name", "type")
@@ -156,10 +158,12 @@ class WatchDirResponse(_message.Message):
) -> None: ...
class CreateWatcherRequest(_message.Message):
- __slots__ = ("path",)
+ __slots__ = ("path", "recursive")
PATH_FIELD_NUMBER: _ClassVar[int]
+ RECURSIVE_FIELD_NUMBER: _ClassVar[int]
path: str
- def __init__(self, path: _Optional[str] = ...) -> None: ...
+ recursive: bool
+ def __init__(self, path: _Optional[str] = ..., recursive: bool = ...) -> None: ...
class CreateWatcherResponse(_message.Message):
__slots__ = ("watcher_id",)
diff --git a/packages/python-sdk/e2b/envd/process/process_pb2.py b/packages/python-sdk/e2b/envd/process/process_pb2.py
index 1674f451a..4ef9098da 100644
--- a/packages/python-sdk/e2b/envd/process/process_pb2.py
+++ b/packages/python-sdk/e2b/envd/process/process_pb2.py
@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
# source: process/process.proto
-# Protobuf Python Version: 5.27.3
+# Protobuf Python Version: 5.28.3
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
@@ -22,9 +23,9 @@
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "process.process_pb2", _globals)
if not _descriptor._USE_C_DESCRIPTORS:
_globals["DESCRIPTOR"]._loaded_options = None
- _globals["DESCRIPTOR"]._serialized_options = (
- b"\n\013com.processB\014ProcessProtoP\001\242\002\003PXX\252\002\007Process\312\002\007Process\342\002\023Process\\GPBMetadata\352\002\007Process"
- )
+ _globals[
+ "DESCRIPTOR"
+ ]._serialized_options = b"\n\013com.processB\014ProcessProtoP\001\242\002\003PXX\252\002\007Process\312\002\007Process\342\002\023Process\\GPBMetadata\352\002\007Process"
_globals["_PROCESSCONFIG_ENVSENTRY"]._loaded_options = None
_globals["_PROCESSCONFIG_ENVSENTRY"]._serialized_options = b"8\001"
_globals["_SIGNAL"]._serialized_start = 2316
diff --git a/packages/python-sdk/e2b/sandbox/main.py b/packages/python-sdk/e2b/sandbox/main.py
index a87cbe07e..539a0ae7f 100644
--- a/packages/python-sdk/e2b/sandbox/main.py
+++ b/packages/python-sdk/e2b/sandbox/main.py
@@ -22,15 +22,18 @@ class SandboxSetup(ABC):
@property
@abstractmethod
- def connection_config(self) -> ConnectionConfig: ...
+ def connection_config(self) -> ConnectionConfig:
+ ...
@property
@abstractmethod
- def envd_api_url(self) -> str: ...
+ def envd_api_url(self) -> str:
+ ...
@property
@abstractmethod
- def sandbox_id(self) -> str: ...
+ def sandbox_id(self) -> str:
+ ...
def _file_url(self, path: Optional[str] = None) -> str:
url = urllib.parse.urljoin(self.envd_api_url, ENVD_API_FILES_ROUTE)
diff --git a/packages/python-sdk/e2b/sandbox_async/filesystem/filesystem.py b/packages/python-sdk/e2b/sandbox_async/filesystem/filesystem.py
index 216147c78..7210b99bc 100644
--- a/packages/python-sdk/e2b/sandbox_async/filesystem/filesystem.py
+++ b/packages/python-sdk/e2b/sandbox_async/filesystem/filesystem.py
@@ -1,9 +1,10 @@
+import httpcore
+import httpx
from io import TextIOBase
+from packaging.version import Version
from typing import IO, AsyncIterator, List, Literal, Optional, Union, overload
import e2b_connect as connect
-import httpcore
-import httpx
from e2b.connection_config import (
ConnectionConfig,
Username,
@@ -13,7 +14,7 @@
from e2b.envd.api import ENVD_API_FILES_ROUTE, ahandle_envd_api_exception
from e2b.envd.filesystem import filesystem_connect, filesystem_pb2
from e2b.envd.rpc import authentication_header, handle_rpc_exception
-from e2b.exceptions import SandboxException
+from e2b.exceptions import SandboxException, TemplateException
from e2b.sandbox.filesystem.filesystem import EntryInfo, map_file_type
from e2b.sandbox.filesystem.watch_handle import FilesystemEvent
from e2b.sandbox_async.filesystem.watch_handle import AsyncWatchHandle
@@ -28,11 +29,13 @@ class Filesystem:
def __init__(
self,
envd_api_url: str,
+ envd_version: Optional[str],
connection_config: ConnectionConfig,
pool: httpcore.AsyncConnectionPool,
envd_api: httpx.AsyncClient,
) -> None:
self._envd_api_url = envd_api_url
+ self._envd_version = envd_version
self._connection_config = connection_config
self._pool = pool
self._envd_api = envd_api
@@ -343,6 +346,7 @@ async def watch_dir(
user: Username = "user",
request_timeout: Optional[float] = None,
timeout: Optional[float] = 60,
+ recursive: bool = False,
) -> AsyncWatchHandle:
"""
Watch directory for filesystem events.
@@ -353,11 +357,18 @@ async def watch_dir(
:param user: Run the operation as this user
:param request_timeout: Timeout for the request in **seconds**
:param timeout: Timeout for the watch operation in **seconds**. Using `0` will not limit the watch time
+ :param recursive: Watch directory recursively
:return: `AsyncWatchHandle` object for stopping watching directory
"""
+ if recursive and self._envd_version is not None and Version(self._envd_version) < Version("0.1.3"):
+ raise TemplateException(
+ "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."
+ )
+
events = self._rpc.awatch_dir(
- filesystem_pb2.WatchDirRequest(path=path),
+ filesystem_pb2.WatchDirRequest(path=path, recursive=recursive),
request_timeout=self._connection_config.get_request_timeout(
request_timeout
),
diff --git a/packages/python-sdk/e2b/sandbox_async/main.py b/packages/python-sdk/e2b/sandbox_async/main.py
index 066162d85..a18e1a3bf 100644
--- a/packages/python-sdk/e2b/sandbox_async/main.py
+++ b/packages/python-sdk/e2b/sandbox_async/main.py
@@ -31,6 +31,7 @@ async def handle_async_request(self, request):
class AsyncSandboxOpts(TypedDict):
sandbox_id: str
+ envd_version: Optional[str]
connection_config: ConnectionConfig
@@ -103,6 +104,7 @@ def __init__(self, **opts: Unpack[AsyncSandboxOpts]):
self._connection_config = opts["connection_config"]
self._envd_api_url = f"{'http' if self.connection_config.debug else 'https'}://{self.get_host(self.envd_port)}"
+ self._envd_version = opts["envd_version"]
self._transport = AsyncTransportWithLogger(limits=self._limits)
self._envd_api = httpx.AsyncClient(
@@ -112,6 +114,7 @@ def __init__(self, **opts: Unpack[AsyncSandboxOpts]):
self._filesystem = Filesystem(
self.envd_api_url,
+ self._envd_version,
self.connection_config,
self._transport._pool,
self._envd_api,
@@ -198,8 +201,8 @@ async def create(
request_timeout=request_timeout,
)
- sandbox_id = (
- "debug_sandbox_id"
+ sandbox_id, envd_version = (
+ "debug_sandbox_id", None
if connection_config.debug
else await SandboxApi._create_sandbox(
template=template or cls.default_template,
@@ -215,6 +218,7 @@ async def create(
return cls(
sandbox_id=sandbox_id,
+ envd_version=envd_version,
connection_config=connection_config,
)
@@ -251,6 +255,7 @@ async def connect(
return cls(
sandbox_id=sandbox_id,
+ envd_version=None,
connection_config=connection_config,
)
diff --git a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py
index ea99105cc..14c4430e6 100644
--- a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py
+++ b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py
@@ -1,4 +1,4 @@
-from typing import Optional, Dict, List
+from typing import Optional, Dict, List, Tuple
from packaging.version import Version
from e2b.sandbox.sandbox_api import SandboxInfo, SandboxApiBase
@@ -142,7 +142,7 @@ async def _create_sandbox(
domain: Optional[str] = None,
debug: Optional[bool] = None,
request_timeout: Optional[float] = None,
- ) -> str:
+ ) -> Tuple[str, str]:
config = ConnectionConfig(
api_key=api_key,
domain=domain,
@@ -182,7 +182,7 @@ async def _create_sandbox(
return SandboxApi._get_sandbox_id(
res.parsed.sandbox_id,
res.parsed.client_id,
- )
+ ), res.parsed.envd_version
@staticmethod
def _get_sandbox_id(sandbox_id: str, client_id: str) -> str:
diff --git a/packages/python-sdk/e2b/sandbox_sync/filesystem/filesystem.py b/packages/python-sdk/e2b/sandbox_sync/filesystem/filesystem.py
index 094d82a71..5945226ae 100644
--- a/packages/python-sdk/e2b/sandbox_sync/filesystem/filesystem.py
+++ b/packages/python-sdk/e2b/sandbox_sync/filesystem/filesystem.py
@@ -4,6 +4,9 @@
import e2b_connect
import httpcore
import httpx
+from packaging.version import Version
+
+from e2b.exceptions import TemplateException
from e2b.connection_config import (
ConnectionConfig,
Username,
@@ -25,11 +28,13 @@ class Filesystem:
def __init__(
self,
envd_api_url: str,
+ envd_version: Optional[str],
connection_config: ConnectionConfig,
pool: httpcore.ConnectionPool,
envd_api: httpx.Client,
) -> None:
self._envd_api_url = envd_api_url
+ self._envd_version = envd_version
self._connection_config = connection_config
self._pool = pool
self._envd_api = envd_api
@@ -336,6 +341,7 @@ def watch_dir(
path: str,
user: Username = "user",
request_timeout: Optional[float] = None,
+ recursive: bool = False,
) -> WatchHandle:
"""
Watch directory for filesystem events.
@@ -343,12 +349,19 @@ def watch_dir(
:param path: Path to a directory to watch
:param user: Run the operation as this user
:param request_timeout: Timeout for the request in **seconds**
+ :param recursive: Watch directory recursively
:return: `WatchHandle` object for stopping watching directory
"""
+ if recursive and self._envd_version is not None and Version(self._envd_version) < Version("0.1.3"):
+ raise TemplateException(
+ "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."
+ )
+
try:
r = self._rpc.create_watcher(
- filesystem_pb2.CreateWatcherRequest(path=path),
+ filesystem_pb2.CreateWatcherRequest(path=path, recursive=recursive),
request_timeout=self._connection_config.get_request_timeout(
request_timeout
),
diff --git a/packages/python-sdk/e2b/sandbox_sync/main.py b/packages/python-sdk/e2b/sandbox_sync/main.py
index 2fa54ea86..31b85f655 100644
--- a/packages/python-sdk/e2b/sandbox_sync/main.py
+++ b/packages/python-sdk/e2b/sandbox_sync/main.py
@@ -129,12 +129,14 @@ def __init__(
if self.connection_config.debug:
self._sandbox_id = "debug_sandbox_id"
+ self._envd_version = None
elif sandbox_id is not None:
self._sandbox_id = sandbox_id
+ self._envd_version = None
else:
template = template or self.default_template
timeout = timeout or self.default_sandbox_timeout
- self._sandbox_id = SandboxApi._create_sandbox(
+ self._sandbox_id, self._envd_version = SandboxApi._create_sandbox(
template=template,
api_key=api_key,
timeout=timeout,
@@ -155,6 +157,7 @@ def __init__(
self._filesystem = Filesystem(
self.envd_api_url,
+ self._envd_version,
self.connection_config,
self._transport._pool,
self._envd_api,
diff --git a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py
index 8e37aab02..e3c5d6a22 100644
--- a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py
+++ b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py
@@ -1,5 +1,5 @@
from httpx import HTTPTransport
-from typing import Optional, Dict, List
+from typing import Optional, Dict, List, Tuple
from packaging.version import Version
from e2b.sandbox.sandbox_api import SandboxInfo, SandboxApiBase
@@ -149,7 +149,7 @@ def _create_sandbox(
domain: Optional[str] = None,
debug: Optional[bool] = None,
request_timeout: Optional[float] = None,
- ) -> str:
+ ) -> Tuple[str, str]:
config = ConnectionConfig(
api_key=api_key,
domain=domain,
@@ -191,4 +191,4 @@ def _create_sandbox(
return SandboxApi._get_sandbox_id(
res.parsed.sandbox_id,
res.parsed.client_id,
- )
+ ), res.parsed.envd_version
diff --git a/packages/python-sdk/tests/async/sandbox_async/files/test_watch.py b/packages/python-sdk/tests/async/sandbox_async/files/test_watch.py
index 19d319a95..af1f56663 100644
--- a/packages/python-sdk/tests/async/sandbox_async/files/test_watch.py
+++ b/packages/python-sdk/tests/async/sandbox_async/files/test_watch.py
@@ -35,6 +35,70 @@ def handle_event(e: FilesystemEvent):
await handle.stop()
+async def test_watch_recursive_directory_changes(async_sandbox: AsyncSandbox):
+ dirname = "test_recursive_watch_dir"
+ nested_dirname = "test_nested_watch_dir"
+ filename = "test_watch.txt"
+ content = "This file will be watched."
+
+ await async_sandbox.files.remove(dirname)
+ await async_sandbox.files.make_dir(f"{dirname}/{nested_dirname}")
+
+ event_triggered = Event()
+
+ expected_filename = f"{nested_dirname}/{filename}"
+
+ def handle_event(e: FilesystemEvent):
+ if e.type == FilesystemEventType.WRITE and e.name == expected_filename:
+ event_triggered.set()
+
+ handle = await async_sandbox.files.watch_dir(
+ dirname, on_event=handle_event, recursive=True
+ )
+
+ await async_sandbox.files.write(f"{dirname}/{nested_dirname}/{filename}", content)
+
+ await event_triggered.wait()
+
+ await handle.stop()
+
+
+async def test_watch_recursive_directory_after_nested_folder_addition(
+ async_sandbox: AsyncSandbox,
+):
+ dirname = "test_recursive_watch_dir_add"
+ nested_dirname = "test_nested_watch_dir"
+ filename = "test_watch.txt"
+ content = "This file will be watched."
+
+ await async_sandbox.files.remove(dirname)
+ await async_sandbox.files.make_dir(dirname)
+
+ event_triggered_file = Event()
+ event_triggered_folder = Event()
+
+ expected_filename = f"{nested_dirname}/{filename}"
+
+ def handle_event(e: FilesystemEvent):
+ if e.type == FilesystemEventType.WRITE and e.name == expected_filename:
+ event_triggered_file.set()
+ return
+ if e.type == FilesystemEventType.CREATE and e.name == nested_dirname:
+ event_triggered_folder.set()
+
+ handle = await async_sandbox.files.watch_dir(
+ dirname, on_event=handle_event, recursive=True
+ )
+
+ await async_sandbox.files.make_dir(f"{dirname}/{nested_dirname}")
+ await event_triggered_folder.wait()
+
+ await async_sandbox.files.write(f"{dirname}/{nested_dirname}/{filename}", content)
+ await event_triggered_file.wait()
+
+ await handle.stop()
+
+
async def test_watch_non_existing_directory(async_sandbox: AsyncSandbox):
dirname = "non_existing_watch_dir"
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..859fc4ec1 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
@@ -8,7 +8,9 @@ def test_watch_directory_changes(sandbox: Sandbox):
filename = "test_watch.txt"
content = "This file will be watched."
+ sandbox.files.remove(dirname)
sandbox.files.make_dir(dirname)
+
handle = sandbox.files.watch_dir(dirname)
sandbox.files.write(f"{dirname}/{filename}", content)
@@ -46,6 +48,59 @@ def test_watch_iterated(sandbox: Sandbox):
handle.stop()
+def test_watch_recursive_directory_changes(sandbox: Sandbox):
+ dirname = "test_recursive_watch_dir"
+ nested_dirname = "test_nested_watch_dir"
+ filename = "test_watch.txt"
+ content = "This file will be watched."
+
+ sandbox.files.remove(dirname)
+ sandbox.files.make_dir(f"{dirname}/{nested_dirname}")
+
+ handle = sandbox.files.watch_dir(dirname, recursive=True)
+ sandbox.files.write(f"{dirname}/{nested_dirname}/{filename}", content)
+
+ events = handle.get_new_events()
+ assert len(events) == 3
+ expected_filename = f"{nested_dirname}/{filename}"
+ assert events[0].type == FilesystemEventType.CREATE
+ assert events[0].name == expected_filename
+
+ handle.stop()
+
+
+def test_watch_recursive_directory_after_nested_folder_addition(sandbox: Sandbox):
+ dirname = "test_recursive_watch_dir_add"
+ nested_dirname = "test_nested_watch_dir"
+ filename = "test_watch.txt"
+ content = "This file will be watched."
+
+ sandbox.files.remove(dirname)
+ sandbox.files.make_dir(dirname)
+
+ handle = sandbox.files.watch_dir(dirname, recursive=True)
+
+ sandbox.files.make_dir(f"{dirname}/{nested_dirname}")
+ sandbox.files.write(f"{dirname}/{nested_dirname}/{filename}", content)
+
+ expected_filename = f"{nested_dirname}/{filename}"
+
+ events = handle.get_new_events()
+ file_changed = False
+ folder_created = False
+ for event in events:
+ if event.type == FilesystemEventType.WRITE and event.name == expected_filename:
+ file_changed = True
+ continue
+ if event.type == FilesystemEventType.CREATE and event.name == nested_dirname:
+ folder_created = True
+
+ assert folder_created
+ assert file_changed
+
+ handle.stop()
+
+
def test_watch_non_existing_directory(sandbox: Sandbox):
dirname = "non_existing_watch_dir"
diff --git a/spec/envd/filesystem/filesystem.proto b/spec/envd/filesystem/filesystem.proto
index 05694cb20..8c71c4f20 100644
--- a/spec/envd/filesystem/filesystem.proto
+++ b/spec/envd/filesystem/filesystem.proto
@@ -70,6 +70,7 @@ message ListDirResponse {
message WatchDirRequest {
string path = 1;
+ bool recursive = 2;
}
message FilesystemEvent {
@@ -91,6 +92,7 @@ message WatchDirResponse {
message CreateWatcherRequest {
string path = 1;
+ bool recursive = 2;
}
message CreateWatcherResponse {
@@ -106,7 +108,7 @@ message GetWatcherEventsResponse {
}
message RemoveWatcherRequest {
- string watcher_id = 1;
+ string watcher_id = 1;
}
message RemoveWatcherResponse {}