From 2005cd9fe5081a9179f53ce6947089d1247b51a7 Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Thu, 4 Jun 2026 20:53:02 +0200 Subject: [PATCH 1/2] feat(sdk): add includeEntry option to filesystem watch Mirror infra PR e2b-dev/infra#2930 on the client: add an includeEntry/include_entry option to directory watching across the JS and Python (sync + async) SDKs. When enabled, each FilesystemEvent carries the affected entry's EntryInfo (best-effort; unset for remove/rename-away events). Regenerated the filesystem proto code and added tests plus a changeset. Co-Authored-By: Claude Opus 4.8 --- .changeset/watch-include-entry.md | 6 ++ .../src/envd/filesystem/filesystem_pb.ts | 25 +++++- .../js-sdk/src/sandbox/filesystem/index.ts | 76 +++++++++---------- .../src/sandbox/filesystem/watchHandle.ts | 12 +++ .../js-sdk/tests/sandbox/files/watch.test.ts | 39 ++++++++++ .../e2b/envd/filesystem/filesystem_pb2.py | 56 +++++++------- .../e2b/envd/filesystem/filesystem_pb2.pyi | 31 ++++++-- .../e2b/sandbox/filesystem/filesystem.py | 18 +++++ .../e2b/sandbox/filesystem/watch_handle.py | 10 +++ .../sandbox_async/filesystem/filesystem.py | 50 +++--------- .../sandbox_async/filesystem/watch_handle.py | 6 ++ .../e2b/sandbox_sync/filesystem/filesystem.py | 68 +++-------------- .../sandbox_sync/filesystem/watch_handle.py | 6 ++ .../async/sandbox_async/files/test_watch.py | 36 +++++++++ .../sync/sandbox_sync/files/test_watch.py | 39 +++++++++- spec/envd/filesystem/filesystem.proto | 8 ++ 16 files changed, 314 insertions(+), 172 deletions(-) create mode 100644 .changeset/watch-include-entry.md diff --git a/.changeset/watch-include-entry.md b/.changeset/watch-include-entry.md new file mode 100644 index 0000000000..b8ed420334 --- /dev/null +++ b/.changeset/watch-include-entry.md @@ -0,0 +1,6 @@ +--- +"@e2b/python-sdk": minor +"e2b": minor +--- + +Add an `includeEntry`/`include_entry` option to filesystem directory watching. When enabled, each `FilesystemEvent` carries the affected entry's `EntryInfo` (best-effort; left unset for events where the path no longer exists, such as remove/rename-away). Requires envd 0.6.2 or later; older sandboxes ignore the option and leave `entry` unset. diff --git a/packages/js-sdk/src/envd/filesystem/filesystem_pb.ts b/packages/js-sdk/src/envd/filesystem/filesystem_pb.ts index 06138291c4..b3065b2a90 100644 --- a/packages/js-sdk/src/envd/filesystem/filesystem_pb.ts +++ b/packages/js-sdk/src/envd/filesystem/filesystem_pb.ts @@ -24,7 +24,7 @@ import type { Message } from '@bufbuild/protobuf' export const file_filesystem_filesystem: GenFile = /*@__PURE__*/ fileDesc( - 'ChtmaWxlc3lzdGVtL2ZpbGVzeXN0ZW0ucHJvdG8SCmZpbGVzeXN0ZW0iMgoLTW92ZVJlcXVlc3QSDgoGc291cmNlGAEgASgJEhMKC2Rlc3RpbmF0aW9uGAIgASgJIjQKDE1vdmVSZXNwb25zZRIkCgVlbnRyeRgBIAEoCzIVLmZpbGVzeXN0ZW0uRW50cnlJbmZvIh4KDk1ha2VEaXJSZXF1ZXN0EgwKBHBhdGgYASABKAkiNwoPTWFrZURpclJlc3BvbnNlEiQKBWVudHJ5GAEgASgLMhUuZmlsZXN5c3RlbS5FbnRyeUluZm8iHQoNUmVtb3ZlUmVxdWVzdBIMCgRwYXRoGAEgASgJIhAKDlJlbW92ZVJlc3BvbnNlIhsKC1N0YXRSZXF1ZXN0EgwKBHBhdGgYASABKAkiNAoMU3RhdFJlc3BvbnNlEiQKBWVudHJ5GAEgASgLMhUuZmlsZXN5c3RlbS5FbnRyeUluZm8i/QEKCUVudHJ5SW5mbxIMCgRuYW1lGAEgASgJEiIKBHR5cGUYAiABKA4yFC5maWxlc3lzdGVtLkZpbGVUeXBlEgwKBHBhdGgYAyABKAkSDAoEc2l6ZRgEIAEoAxIMCgRtb2RlGAUgASgNEhMKC3Blcm1pc3Npb25zGAYgASgJEg0KBW93bmVyGAcgASgJEg0KBWdyb3VwGAggASgJEjEKDW1vZGlmaWVkX3RpbWUYCSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhsKDnN5bWxpbmtfdGFyZ2V0GAogASgJSACIAQFCEQoPX3N5bWxpbmtfdGFyZ2V0Ii0KDkxpc3REaXJSZXF1ZXN0EgwKBHBhdGgYASABKAkSDQoFZGVwdGgYAiABKA0iOQoPTGlzdERpclJlc3BvbnNlEiYKB2VudHJpZXMYASADKAsyFS5maWxlc3lzdGVtLkVudHJ5SW5mbyIyCg9XYXRjaERpclJlcXVlc3QSDAoEcGF0aBgBIAEoCRIRCglyZWN1cnNpdmUYAiABKAgiRAoPRmlsZXN5c3RlbUV2ZW50EgwKBG5hbWUYASABKAkSIwoEdHlwZRgCIAEoDjIVLmZpbGVzeXN0ZW0uRXZlbnRUeXBlIuABChBXYXRjaERpclJlc3BvbnNlEjgKBXN0YXJ0GAEgASgLMicuZmlsZXN5c3RlbS5XYXRjaERpclJlc3BvbnNlLlN0YXJ0RXZlbnRIABIxCgpmaWxlc3lzdGVtGAIgASgLMhsuZmlsZXN5c3RlbS5GaWxlc3lzdGVtRXZlbnRIABI7CglrZWVwYWxpdmUYAyABKAsyJi5maWxlc3lzdGVtLldhdGNoRGlyUmVzcG9uc2UuS2VlcEFsaXZlSAAaDAoKU3RhcnRFdmVudBoLCglLZWVwQWxpdmVCBwoFZXZlbnQiNwoUQ3JlYXRlV2F0Y2hlclJlcXVlc3QSDAoEcGF0aBgBIAEoCRIRCglyZWN1cnNpdmUYAiABKAgiKwoVQ3JlYXRlV2F0Y2hlclJlc3BvbnNlEhIKCndhdGNoZXJfaWQYASABKAkiLQoXR2V0V2F0Y2hlckV2ZW50c1JlcXVlc3QSEgoKd2F0Y2hlcl9pZBgBIAEoCSJHChhHZXRXYXRjaGVyRXZlbnRzUmVzcG9uc2USKwoGZXZlbnRzGAEgAygLMhsuZmlsZXN5c3RlbS5GaWxlc3lzdGVtRXZlbnQiKgoUUmVtb3ZlV2F0Y2hlclJlcXVlc3QSEgoKd2F0Y2hlcl9pZBgBIAEoCSIXChVSZW1vdmVXYXRjaGVyUmVzcG9uc2UqUgoIRmlsZVR5cGUSGQoVRklMRV9UWVBFX1VOU1BFQ0lGSUVEEAASEgoORklMRV9UWVBFX0ZJTEUQARIXChNGSUxFX1RZUEVfRElSRUNUT1JZEAIqmAEKCUV2ZW50VHlwZRIaChZFVkVOVF9UWVBFX1VOU1BFQ0lGSUVEEAASFQoRRVZFTlRfVFlQRV9DUkVBVEUQARIUChBFVkVOVF9UWVBFX1dSSVRFEAISFQoRRVZFTlRfVFlQRV9SRU1PVkUQAxIVChFFVkVOVF9UWVBFX1JFTkFNRRAEEhQKEEVWRU5UX1RZUEVfQ0hNT0QQBTKfBQoKRmlsZXN5c3RlbRI5CgRTdGF0EhcuZmlsZXN5c3RlbS5TdGF0UmVxdWVzdBoYLmZpbGVzeXN0ZW0uU3RhdFJlc3BvbnNlEkIKB01ha2VEaXISGi5maWxlc3lzdGVtLk1ha2VEaXJSZXF1ZXN0GhsuZmlsZXN5c3RlbS5NYWtlRGlyUmVzcG9uc2USOQoETW92ZRIXLmZpbGVzeXN0ZW0uTW92ZVJlcXVlc3QaGC5maWxlc3lzdGVtLk1vdmVSZXNwb25zZRJCCgdMaXN0RGlyEhouZmlsZXN5c3RlbS5MaXN0RGlyUmVxdWVzdBobLmZpbGVzeXN0ZW0uTGlzdERpclJlc3BvbnNlEj8KBlJlbW92ZRIZLmZpbGVzeXN0ZW0uUmVtb3ZlUmVxdWVzdBoaLmZpbGVzeXN0ZW0uUmVtb3ZlUmVzcG9uc2USRwoIV2F0Y2hEaXISGy5maWxlc3lzdGVtLldhdGNoRGlyUmVxdWVzdBocLmZpbGVzeXN0ZW0uV2F0Y2hEaXJSZXNwb25zZTABElQKDUNyZWF0ZVdhdGNoZXISIC5maWxlc3lzdGVtLkNyZWF0ZVdhdGNoZXJSZXF1ZXN0GiEuZmlsZXN5c3RlbS5DcmVhdGVXYXRjaGVyUmVzcG9uc2USXQoQR2V0V2F0Y2hlckV2ZW50cxIjLmZpbGVzeXN0ZW0uR2V0V2F0Y2hlckV2ZW50c1JlcXVlc3QaJC5maWxlc3lzdGVtLkdldFdhdGNoZXJFdmVudHNSZXNwb25zZRJUCg1SZW1vdmVXYXRjaGVyEiAuZmlsZXN5c3RlbS5SZW1vdmVXYXRjaGVyUmVxdWVzdBohLmZpbGVzeXN0ZW0uUmVtb3ZlV2F0Y2hlclJlc3BvbnNlQmkKDmNvbS5maWxlc3lzdGVtQg9GaWxlc3lzdGVtUHJvdG9QAaICA0ZYWKoCCkZpbGVzeXN0ZW3KAgpGaWxlc3lzdGVt4gIWRmlsZXN5c3RlbVxHUEJNZXRhZGF0YeoCCkZpbGVzeXN0ZW1iBnByb3RvMw', + 'ChtmaWxlc3lzdGVtL2ZpbGVzeXN0ZW0ucHJvdG8SCmZpbGVzeXN0ZW0iMgoLTW92ZVJlcXVlc3QSDgoGc291cmNlGAEgASgJEhMKC2Rlc3RpbmF0aW9uGAIgASgJIjQKDE1vdmVSZXNwb25zZRIkCgVlbnRyeRgBIAEoCzIVLmZpbGVzeXN0ZW0uRW50cnlJbmZvIh4KDk1ha2VEaXJSZXF1ZXN0EgwKBHBhdGgYASABKAkiNwoPTWFrZURpclJlc3BvbnNlEiQKBWVudHJ5GAEgASgLMhUuZmlsZXN5c3RlbS5FbnRyeUluZm8iHQoNUmVtb3ZlUmVxdWVzdBIMCgRwYXRoGAEgASgJIhAKDlJlbW92ZVJlc3BvbnNlIhsKC1N0YXRSZXF1ZXN0EgwKBHBhdGgYASABKAkiNAoMU3RhdFJlc3BvbnNlEiQKBWVudHJ5GAEgASgLMhUuZmlsZXN5c3RlbS5FbnRyeUluZm8i/QEKCUVudHJ5SW5mbxIMCgRuYW1lGAEgASgJEiIKBHR5cGUYAiABKA4yFC5maWxlc3lzdGVtLkZpbGVUeXBlEgwKBHBhdGgYAyABKAkSDAoEc2l6ZRgEIAEoAxIMCgRtb2RlGAUgASgNEhMKC3Blcm1pc3Npb25zGAYgASgJEg0KBW93bmVyGAcgASgJEg0KBWdyb3VwGAggASgJEjEKDW1vZGlmaWVkX3RpbWUYCSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhsKDnN5bWxpbmtfdGFyZ2V0GAogASgJSACIAQFCEQoPX3N5bWxpbmtfdGFyZ2V0Ii0KDkxpc3REaXJSZXF1ZXN0EgwKBHBhdGgYASABKAkSDQoFZGVwdGgYAiABKA0iOQoPTGlzdERpclJlc3BvbnNlEiYKB2VudHJpZXMYASADKAsyFS5maWxlc3lzdGVtLkVudHJ5SW5mbyJJCg9XYXRjaERpclJlcXVlc3QSDAoEcGF0aBgBIAEoCRIRCglyZWN1cnNpdmUYAiABKAgSFQoNaW5jbHVkZV9lbnRyeRgDIAEoCCJ5Cg9GaWxlc3lzdGVtRXZlbnQSDAoEbmFtZRgBIAEoCRIjCgR0eXBlGAIgASgOMhUuZmlsZXN5c3RlbS5FdmVudFR5cGUSKQoFZW50cnkYAyABKAsyFS5maWxlc3lzdGVtLkVudHJ5SW5mb0gAiAEBQggKBl9lbnRyeSLgAQoQV2F0Y2hEaXJSZXNwb25zZRI4CgVzdGFydBgBIAEoCzInLmZpbGVzeXN0ZW0uV2F0Y2hEaXJSZXNwb25zZS5TdGFydEV2ZW50SAASMQoKZmlsZXN5c3RlbRgCIAEoCzIbLmZpbGVzeXN0ZW0uRmlsZXN5c3RlbUV2ZW50SAASOwoJa2VlcGFsaXZlGAMgASgLMiYuZmlsZXN5c3RlbS5XYXRjaERpclJlc3BvbnNlLktlZXBBbGl2ZUgAGgwKClN0YXJ0RXZlbnQaCwoJS2VlcEFsaXZlQgcKBWV2ZW50Ik4KFENyZWF0ZVdhdGNoZXJSZXF1ZXN0EgwKBHBhdGgYASABKAkSEQoJcmVjdXJzaXZlGAIgASgIEhUKDWluY2x1ZGVfZW50cnkYAyABKAgiKwoVQ3JlYXRlV2F0Y2hlclJlc3BvbnNlEhIKCndhdGNoZXJfaWQYASABKAkiLQoXR2V0V2F0Y2hlckV2ZW50c1JlcXVlc3QSEgoKd2F0Y2hlcl9pZBgBIAEoCSJHChhHZXRXYXRjaGVyRXZlbnRzUmVzcG9uc2USKwoGZXZlbnRzGAEgAygLMhsuZmlsZXN5c3RlbS5GaWxlc3lzdGVtRXZlbnQiKgoUUmVtb3ZlV2F0Y2hlclJlcXVlc3QSEgoKd2F0Y2hlcl9pZBgBIAEoCSIXChVSZW1vdmVXYXRjaGVyUmVzcG9uc2UqUgoIRmlsZVR5cGUSGQoVRklMRV9UWVBFX1VOU1BFQ0lGSUVEEAASEgoORklMRV9UWVBFX0ZJTEUQARIXChNGSUxFX1RZUEVfRElSRUNUT1JZEAIqmAEKCUV2ZW50VHlwZRIaChZFVkVOVF9UWVBFX1VOU1BFQ0lGSUVEEAASFQoRRVZFTlRfVFlQRV9DUkVBVEUQARIUChBFVkVOVF9UWVBFX1dSSVRFEAISFQoRRVZFTlRfVFlQRV9SRU1PVkUQAxIVChFFVkVOVF9UWVBFX1JFTkFNRRAEEhQKEEVWRU5UX1RZUEVfQ0hNT0QQBTKfBQoKRmlsZXN5c3RlbRI5CgRTdGF0EhcuZmlsZXN5c3RlbS5TdGF0UmVxdWVzdBoYLmZpbGVzeXN0ZW0uU3RhdFJlc3BvbnNlEkIKB01ha2VEaXISGi5maWxlc3lzdGVtLk1ha2VEaXJSZXF1ZXN0GhsuZmlsZXN5c3RlbS5NYWtlRGlyUmVzcG9uc2USOQoETW92ZRIXLmZpbGVzeXN0ZW0uTW92ZVJlcXVlc3QaGC5maWxlc3lzdGVtLk1vdmVSZXNwb25zZRJCCgdMaXN0RGlyEhouZmlsZXN5c3RlbS5MaXN0RGlyUmVxdWVzdBobLmZpbGVzeXN0ZW0uTGlzdERpclJlc3BvbnNlEj8KBlJlbW92ZRIZLmZpbGVzeXN0ZW0uUmVtb3ZlUmVxdWVzdBoaLmZpbGVzeXN0ZW0uUmVtb3ZlUmVzcG9uc2USRwoIV2F0Y2hEaXISGy5maWxlc3lzdGVtLldhdGNoRGlyUmVxdWVzdBocLmZpbGVzeXN0ZW0uV2F0Y2hEaXJSZXNwb25zZTABElQKDUNyZWF0ZVdhdGNoZXISIC5maWxlc3lzdGVtLkNyZWF0ZVdhdGNoZXJSZXF1ZXN0GiEuZmlsZXN5c3RlbS5DcmVhdGVXYXRjaGVyUmVzcG9uc2USXQoQR2V0V2F0Y2hlckV2ZW50cxIjLmZpbGVzeXN0ZW0uR2V0V2F0Y2hlckV2ZW50c1JlcXVlc3QaJC5maWxlc3lzdGVtLkdldFdhdGNoZXJFdmVudHNSZXNwb25zZRJUCg1SZW1vdmVXYXRjaGVyEiAuZmlsZXN5c3RlbS5SZW1vdmVXYXRjaGVyUmVxdWVzdBohLmZpbGVzeXN0ZW0uUmVtb3ZlV2F0Y2hlclJlc3BvbnNlQmkKDmNvbS5maWxlc3lzdGVtQg9GaWxlc3lzdGVtUHJvdG9QAaICA0ZYWKoCCkZpbGVzeXN0ZW3KAgpGaWxlc3lzdGVt4gIWRmlsZXN5c3RlbVxHUEJNZXRhZGF0YeoCCkZpbGVzeXN0ZW1iBnByb3RvMw', [file_google_protobuf_timestamp] ) @@ -291,6 +291,13 @@ export type WatchDirRequest = Message<'filesystem.WatchDirRequest'> & { * @generated from field: bool recursive = 2; */ recursive: boolean + + /** + * If true, each FilesystemEvent includes the EntryInfo of the affected entry, when available. + * + * @generated from field: bool include_entry = 3; + */ + includeEntry: boolean } /** @@ -314,6 +321,15 @@ export type FilesystemEvent = Message<'filesystem.FilesystemEvent'> & { * @generated from field: filesystem.EventType type = 2; */ type: EventType + + /** + * Info of the entry that triggered the event. Only populated when include_entry + * was requested and the entry could be stat-ed (e.g. not set for remove/rename-away + * events, where the entry no longer exists at this path). + * + * @generated from field: optional filesystem.EntryInfo entry = 3; + */ + entry?: EntryInfo } /** @@ -406,6 +422,13 @@ export type CreateWatcherRequest = * @generated from field: bool recursive = 2; */ recursive: boolean + + /** + * If true, each FilesystemEvent includes the EntryInfo of the affected entry, when available. + * + * @generated from field: bool include_entry = 3; + */ + includeEntry: boolean } /** diff --git a/packages/js-sdk/src/sandbox/filesystem/index.ts b/packages/js-sdk/src/sandbox/filesystem/index.ts index 14e7a2e944..0028b0c1f8 100644 --- a/packages/js-sdk/src/sandbox/filesystem/index.ts +++ b/packages/js-sdk/src/sandbox/filesystem/index.ts @@ -20,6 +20,7 @@ import { authenticationHeader, handleRpcError } from '../../envd/rpc' import { EnvdApiClient } from '../../envd/api' import { + EntryInfo as FsEntryInfo, Filesystem as FilesystemService, FileType as FsFileType, } from '../../envd/filesystem/filesystem_pb' @@ -153,6 +154,24 @@ function mapModifiedTime(modifiedTime: Timestamp | undefined) { ) } +/** + * Map a protobuf `EntryInfo` to the SDK `EntryInfo`. + */ +export function mapEntryInfo(entry: FsEntryInfo): EntryInfo { + return { + name: entry.name, + type: mapFileType(entry.type), + path: entry.path, + size: Number(entry.size), + mode: entry.mode, + permissions: entry.permissions, + owner: entry.owner, + group: entry.group, + modifiedTime: mapModifiedTime(entry.modifiedTime), + symlinkTarget: entry.symlinkTarget, + } +} + /** * Options for the sandbox filesystem operations. */ @@ -218,6 +237,16 @@ export interface WatchOpts extends FilesystemRequestOpts { * Watch the directory recursively */ recursive?: boolean + /** + * Include the {@link EntryInfo} of the affected entry in each {@link FilesystemEvent}. + * + * The entry is populated best-effort and may be `undefined` for events where the + * entry no longer exists at the path (e.g. remove or rename-away events). + * + * Requires envd 0.6.2 or later. When the sandbox's envd version doesn't support it, + * the option is ignored and `event.entry` stays `undefined`. + */ + includeEntry?: boolean } /** @@ -577,22 +606,12 @@ export class Filesystem { const entries: EntryInfo[] = [] for (const e of res.entries) { - const type = mapFileType(e.type) - - if (type) { - entries.push({ - name: e.name, - type, - path: e.path, - size: Number(e.size), - mode: e.mode, - permissions: e.permissions, - owner: e.owner, - group: e.group, - modifiedTime: mapModifiedTime(e.modifiedTime), - symlinkTarget: e.symlinkTarget, - }) + // Skip entries with an unknown file type. + if (!mapFileType(e.type)) { + continue } + + entries.push(mapEntryInfo(e)) } return entries @@ -668,18 +687,7 @@ export class Filesystem { throw new Error('Expected to receive information about moved object') } - return { - name: entry.name, - type: mapFileType(entry.type), - path: entry.path, - size: Number(entry.size), - mode: entry.mode, - permissions: entry.permissions, - owner: entry.owner, - group: entry.group, - modifiedTime: mapModifiedTime(entry.modifiedTime), - symlinkTarget: entry.symlinkTarget, - } + return mapEntryInfo(entry) } catch (err) { throw handleFilesystemRpcError(err) } @@ -771,18 +779,7 @@ export class Filesystem { ) } - return { - name: res.entry.name, - type: mapFileType(res.entry.type), - path: res.entry.path, - size: Number(res.entry.size), - mode: res.entry.mode, - permissions: res.entry.permissions, - owner: res.entry.owner, - group: res.entry.group, - modifiedTime: mapModifiedTime(res.entry.modifiedTime), - symlinkTarget: res.entry.symlinkTarget, - } + return mapEntryInfo(res.entry) } catch (err) { throw handleFilesystemRpcError(err) } @@ -827,6 +824,7 @@ export class Filesystem { { path, recursive: opts?.recursive ?? this.defaultWatchRecursive, + includeEntry: opts?.includeEntry ?? false, }, { headers: { diff --git a/packages/js-sdk/src/sandbox/filesystem/watchHandle.ts b/packages/js-sdk/src/sandbox/filesystem/watchHandle.ts index 6d4bd80e83..e29d5703dd 100644 --- a/packages/js-sdk/src/sandbox/filesystem/watchHandle.ts +++ b/packages/js-sdk/src/sandbox/filesystem/watchHandle.ts @@ -3,6 +3,7 @@ import { EventType, WatchDirResponse, } from '../../envd/filesystem/filesystem_pb' +import { EntryInfo, mapEntryInfo } from './index' /** * Sandbox filesystem event types. @@ -57,6 +58,14 @@ export interface FilesystemEvent { * Filesystem operation event type. */ type: FilesystemEventType + /** + * Information about the entry that triggered the event. + * + * Only populated when the watch was started with `includeEntry: true` and the + * sandbox's envd version supports it. It may be `undefined` for events where the + * entry no longer exists at the path (e.g. remove or rename-away events). + */ + entry?: EntryInfo } /** @@ -106,6 +115,9 @@ export class WatchHandle { this.onEvent?.({ name: event.value.name, type: eventType, + entry: event.value.entry + ? mapEntryInfo(event.value.entry) + : undefined, }) } this.onExit?.() diff --git a/packages/js-sdk/tests/sandbox/files/watch.test.ts b/packages/js-sdk/tests/sandbox/files/watch.test.ts index c1f81c6a64..7251242221 100644 --- a/packages/js-sdk/tests/sandbox/files/watch.test.ts +++ b/packages/js-sdk/tests/sandbox/files/watch.test.ts @@ -3,7 +3,9 @@ import { expect, onTestFinished } from 'vitest' import { isDebug, sandboxTest } from '../../setup.js' import { FileNotFoundError, + FilesystemEvent, FilesystemEventType, + FileType, SandboxError, } from '../../../src' @@ -138,6 +140,43 @@ sandboxTest( } ) +sandboxTest('watch directory changes with entry info', async ({ sandbox }) => { + const dirname = 'test_watch_dir_entry' + const filename = 'test_watch.txt' + const content = 'This file will be watched.' + const newContent = 'This file has been modified.' + + await sandbox.files.makeDir(dirname) + await sandbox.files.write(`${dirname}/${filename}`, content) + + let resolveEvent: (event: FilesystemEvent) => void + const eventPromise = new Promise((resolve) => { + resolveEvent = resolve + }) + + const handle = await sandbox.files.watchDir( + dirname, + async (event) => { + if (event.type === FilesystemEventType.WRITE && event.name === filename) { + resolveEvent(event) + } + }, + { includeEntry: true } + ) + + await sandbox.files.write(`${dirname}/${filename}`, newContent) + + const event = await eventPromise + + // The entry is populated best-effort for events where the path still exists. + expect(event.entry).toBeDefined() + expect(event.entry?.name).toBe(filename) + expect(event.entry?.path).toBe(`/home/user/${dirname}/${filename}`) + expect(event.entry?.type).toBe(FileType.FILE) + + await handle.stop() +}) + sandboxTest('watch non-existing directory', async ({ sandbox }) => { const dirname = 'non_existing_watch_dir' diff --git a/packages/python-sdk/e2b/envd/filesystem/filesystem_pb2.py b/packages/python-sdk/e2b/envd/filesystem/filesystem_pb2.py index 54bb90c496..3e67aeedc9 100644 --- a/packages/python-sdk/e2b/envd/filesystem/filesystem_pb2.py +++ b/packages/python-sdk/e2b/envd/filesystem/filesystem_pb2.py @@ -15,7 +15,7 @@ from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1b\x66ilesystem/filesystem.proto\x12\nfilesystem\x1a\x1fgoogle/protobuf/timestamp.proto\"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\"\xd3\x02\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\x12\x12\n\x04size\x18\x04 \x01(\x03R\x04size\x12\x12\n\x04mode\x18\x05 \x01(\rR\x04mode\x12 \n\x0bpermissions\x18\x06 \x01(\tR\x0bpermissions\x12\x14\n\x05owner\x18\x07 \x01(\tR\x05owner\x12\x14\n\x05group\x18\x08 \x01(\tR\x05group\x12?\n\rmodified_time\x18\t \x01(\x0b\x32\x1a.google.protobuf.TimestampR\x0cmodifiedTime\x12*\n\x0esymlink_target\x18\n \x01(\tH\x00R\rsymlinkTarget\x88\x01\x01\x42\x11\n\x0f_symlink_target\":\n\x0eListDirRequest\x12\x12\n\x04path\x18\x01 \x01(\tR\x04path\x12\x14\n\x05\x64\x65pth\x18\x02 \x01(\rR\x05\x64\x65pth\"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') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1b\x66ilesystem/filesystem.proto\x12\nfilesystem\x1a\x1fgoogle/protobuf/timestamp.proto\"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\"\xd3\x02\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\x12\x12\n\x04size\x18\x04 \x01(\x03R\x04size\x12\x12\n\x04mode\x18\x05 \x01(\rR\x04mode\x12 \n\x0bpermissions\x18\x06 \x01(\tR\x0bpermissions\x12\x14\n\x05owner\x18\x07 \x01(\tR\x05owner\x12\x14\n\x05group\x18\x08 \x01(\tR\x05group\x12?\n\rmodified_time\x18\t \x01(\x0b\x32\x1a.google.protobuf.TimestampR\x0cmodifiedTime\x12*\n\x0esymlink_target\x18\n \x01(\tH\x00R\rsymlinkTarget\x88\x01\x01\x42\x11\n\x0f_symlink_target\":\n\x0eListDirRequest\x12\x12\n\x04path\x18\x01 \x01(\tR\x04path\x12\x14\n\x05\x64\x65pth\x18\x02 \x01(\rR\x05\x64\x65pth\"B\n\x0fListDirResponse\x12/\n\x07\x65ntries\x18\x01 \x03(\x0b\x32\x15.filesystem.EntryInfoR\x07\x65ntries\"h\n\x0fWatchDirRequest\x12\x12\n\x04path\x18\x01 \x01(\tR\x04path\x12\x1c\n\trecursive\x18\x02 \x01(\x08R\trecursive\x12#\n\rinclude_entry\x18\x03 \x01(\x08R\x0cincludeEntry\"\x8c\x01\n\x0f\x46ilesystemEvent\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12)\n\x04type\x18\x02 \x01(\x0e\x32\x15.filesystem.EventTypeR\x04type\x12\x30\n\x05\x65ntry\x18\x03 \x01(\x0b\x32\x15.filesystem.EntryInfoH\x00R\x05\x65ntry\x88\x01\x01\x42\x08\n\x06_entry\"\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\"m\n\x14\x43reateWatcherRequest\x12\x12\n\x04path\x18\x01 \x01(\tR\x04path\x12\x1c\n\trecursive\x18\x02 \x01(\x08R\trecursive\x12#\n\rinclude_entry\x18\x03 \x01(\x08R\x0cincludeEntry\"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() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -23,10 +23,10 @@ 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=1690 - _globals['_FILETYPE']._serialized_end=1772 - _globals['_EVENTTYPE']._serialized_start=1775 - _globals['_EVENTTYPE']._serialized_end=1927 + _globals['_FILETYPE']._serialized_start=1825 + _globals['_FILETYPE']._serialized_end=1907 + _globals['_EVENTTYPE']._serialized_start=1910 + _globals['_EVENTTYPE']._serialized_end=2062 _globals['_MOVEREQUEST']._serialized_start=76 _globals['_MOVEREQUEST']._serialized_end=147 _globals['_MOVERESPONSE']._serialized_start=149 @@ -50,27 +50,27 @@ _globals['_LISTDIRRESPONSE']._serialized_start=865 _globals['_LISTDIRRESPONSE']._serialized_end=931 _globals['_WATCHDIRREQUEST']._serialized_start=933 - _globals['_WATCHDIRREQUEST']._serialized_end=1000 - _globals['_FILESYSTEMEVENT']._serialized_start=1002 - _globals['_FILESYSTEMEVENT']._serialized_end=1082 - _globals['_WATCHDIRRESPONSE']._serialized_start=1085 - _globals['_WATCHDIRRESPONSE']._serialized_end=1339 - _globals['_WATCHDIRRESPONSE_STARTEVENT']._serialized_start=1305 - _globals['_WATCHDIRRESPONSE_STARTEVENT']._serialized_end=1317 - _globals['_WATCHDIRRESPONSE_KEEPALIVE']._serialized_start=1319 - _globals['_WATCHDIRRESPONSE_KEEPALIVE']._serialized_end=1330 - _globals['_CREATEWATCHERREQUEST']._serialized_start=1341 - _globals['_CREATEWATCHERREQUEST']._serialized_end=1413 - _globals['_CREATEWATCHERRESPONSE']._serialized_start=1415 - _globals['_CREATEWATCHERRESPONSE']._serialized_end=1469 - _globals['_GETWATCHEREVENTSREQUEST']._serialized_start=1471 - _globals['_GETWATCHEREVENTSREQUEST']._serialized_end=1527 - _globals['_GETWATCHEREVENTSRESPONSE']._serialized_start=1529 - _globals['_GETWATCHEREVENTSRESPONSE']._serialized_end=1608 - _globals['_REMOVEWATCHERREQUEST']._serialized_start=1610 - _globals['_REMOVEWATCHERREQUEST']._serialized_end=1663 - _globals['_REMOVEWATCHERRESPONSE']._serialized_start=1665 - _globals['_REMOVEWATCHERRESPONSE']._serialized_end=1688 - _globals['_FILESYSTEM']._serialized_start=1930 - _globals['_FILESYSTEM']._serialized_end=2601 + _globals['_WATCHDIRREQUEST']._serialized_end=1037 + _globals['_FILESYSTEMEVENT']._serialized_start=1040 + _globals['_FILESYSTEMEVENT']._serialized_end=1180 + _globals['_WATCHDIRRESPONSE']._serialized_start=1183 + _globals['_WATCHDIRRESPONSE']._serialized_end=1437 + _globals['_WATCHDIRRESPONSE_STARTEVENT']._serialized_start=1403 + _globals['_WATCHDIRRESPONSE_STARTEVENT']._serialized_end=1415 + _globals['_WATCHDIRRESPONSE_KEEPALIVE']._serialized_start=1417 + _globals['_WATCHDIRRESPONSE_KEEPALIVE']._serialized_end=1428 + _globals['_CREATEWATCHERREQUEST']._serialized_start=1439 + _globals['_CREATEWATCHERREQUEST']._serialized_end=1548 + _globals['_CREATEWATCHERRESPONSE']._serialized_start=1550 + _globals['_CREATEWATCHERRESPONSE']._serialized_end=1604 + _globals['_GETWATCHEREVENTSREQUEST']._serialized_start=1606 + _globals['_GETWATCHEREVENTSREQUEST']._serialized_end=1662 + _globals['_GETWATCHEREVENTSRESPONSE']._serialized_start=1664 + _globals['_GETWATCHEREVENTSRESPONSE']._serialized_end=1743 + _globals['_REMOVEWATCHERREQUEST']._serialized_start=1745 + _globals['_REMOVEWATCHERREQUEST']._serialized_end=1798 + _globals['_REMOVEWATCHERRESPONSE']._serialized_start=1800 + _globals['_REMOVEWATCHERRESPONSE']._serialized_end=1823 + _globals['_FILESYSTEM']._serialized_start=2065 + _globals['_FILESYSTEM']._serialized_end=2736 # @@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 4770979526..c8886f3154 100644 --- a/packages/python-sdk/e2b/envd/filesystem/filesystem_pb2.pyi +++ b/packages/python-sdk/e2b/envd/filesystem/filesystem_pb2.pyi @@ -154,21 +154,33 @@ class ListDirResponse(_message.Message): ) -> None: ... class WatchDirRequest(_message.Message): - __slots__ = ("path", "recursive") + __slots__ = ("path", "recursive", "include_entry") PATH_FIELD_NUMBER: _ClassVar[int] RECURSIVE_FIELD_NUMBER: _ClassVar[int] + INCLUDE_ENTRY_FIELD_NUMBER: _ClassVar[int] path: str recursive: bool - def __init__(self, path: _Optional[str] = ..., recursive: bool = ...) -> None: ... + include_entry: bool + def __init__( + self, + path: _Optional[str] = ..., + recursive: bool = ..., + include_entry: bool = ..., + ) -> None: ... class FilesystemEvent(_message.Message): - __slots__ = ("name", "type") + __slots__ = ("name", "type", "entry") NAME_FIELD_NUMBER: _ClassVar[int] TYPE_FIELD_NUMBER: _ClassVar[int] + ENTRY_FIELD_NUMBER: _ClassVar[int] name: str type: EventType + entry: EntryInfo def __init__( - self, name: _Optional[str] = ..., type: _Optional[_Union[EventType, str]] = ... + self, + name: _Optional[str] = ..., + type: _Optional[_Union[EventType, str]] = ..., + entry: _Optional[_Union[EntryInfo, _Mapping]] = ..., ) -> None: ... class WatchDirResponse(_message.Message): @@ -195,12 +207,19 @@ class WatchDirResponse(_message.Message): ) -> None: ... class CreateWatcherRequest(_message.Message): - __slots__ = ("path", "recursive") + __slots__ = ("path", "recursive", "include_entry") PATH_FIELD_NUMBER: _ClassVar[int] RECURSIVE_FIELD_NUMBER: _ClassVar[int] + INCLUDE_ENTRY_FIELD_NUMBER: _ClassVar[int] path: str recursive: bool - def __init__(self, path: _Optional[str] = ..., recursive: bool = ...) -> None: ... + include_entry: bool + def __init__( + self, + path: _Optional[str] = ..., + recursive: bool = ..., + include_entry: bool = ..., + ) -> None: ... class CreateWatcherResponse(_message.Message): __slots__ = ("watcher_id",) diff --git a/packages/python-sdk/e2b/sandbox/filesystem/filesystem.py b/packages/python-sdk/e2b/sandbox/filesystem/filesystem.py index 1cee7327a5..073b1ce442 100644 --- a/packages/python-sdk/e2b/sandbox/filesystem/filesystem.py +++ b/packages/python-sdk/e2b/sandbox/filesystem/filesystem.py @@ -88,6 +88,24 @@ class EntryInfo(WriteInfo): """ +def map_entry_info(entry: filesystem_pb2.EntryInfo) -> EntryInfo: + return EntryInfo( + name=entry.name, + type=map_file_type(entry.type), + path=entry.path, + size=entry.size, + mode=entry.mode, + permissions=entry.permissions, + owner=entry.owner, + group=entry.group, + modified_time=entry.modified_time.ToDatetime(), + # Optional, we can't directly access symlink_target otherwise it will be "" instead of None + symlink_target=( + entry.symlink_target if entry.HasField("symlink_target") else None + ), + ) + + class WriteEntry(TypedDict): """ Contains path and data of the file to be written to the filesystem. diff --git a/packages/python-sdk/e2b/sandbox/filesystem/watch_handle.py b/packages/python-sdk/e2b/sandbox/filesystem/watch_handle.py index c92f07df7a..6d6767d4fb 100644 --- a/packages/python-sdk/e2b/sandbox/filesystem/watch_handle.py +++ b/packages/python-sdk/e2b/sandbox/filesystem/watch_handle.py @@ -1,7 +1,9 @@ from dataclasses import dataclass from enum import Enum +from typing import Optional from e2b.envd.filesystem.filesystem_pb2 import EventType +from e2b.sandbox.filesystem.filesystem import EntryInfo class FilesystemEventType(Enum): @@ -58,3 +60,11 @@ class FilesystemEvent: """ Filesystem operation event type. """ + entry: Optional[EntryInfo] = None + """ + Information about the entry that triggered the event. + + Only populated when the watch was started with `include_entry=True` and the + sandbox's envd version supports it. It may be `None` for events where the entry + no longer exists at the path (e.g. remove or rename-away events). + """ diff --git a/packages/python-sdk/e2b/sandbox_async/filesystem/filesystem.py b/packages/python-sdk/e2b/sandbox_async/filesystem/filesystem.py index 309f4f6169..7d640c35ac 100644 --- a/packages/python-sdk/e2b/sandbox_async/filesystem/filesystem.py +++ b/packages/python-sdk/e2b/sandbox_async/filesystem/filesystem.py @@ -33,6 +33,7 @@ EntryInfo, WriteEntry, WriteInfo, + map_entry_info, map_file_type, to_upload_body, ) @@ -382,28 +383,9 @@ async def list( entries: List[EntryInfo] = [] for entry in res.entries: - event_type = map_file_type(entry.type) - - if event_type: - entries.append( - EntryInfo( - name=entry.name, - type=event_type, - path=entry.path, - size=entry.size, - mode=entry.mode, - permissions=entry.permissions, - owner=entry.owner, - group=entry.group, - modified_time=entry.modified_time.ToDatetime(), - # Optional, we can't directly access symlink_target otherwise if will be "" instead of None - symlink_target=( - entry.symlink_target - if entry.HasField("symlink_target") - else None - ), - ) - ) + # Skip entries with an unknown file type. + if map_file_type(entry.type): + entries.append(map_entry_info(entry)) return entries except Exception as e: @@ -537,23 +519,7 @@ async def rename( headers=authentication_header(self._envd_version, user), ) - return EntryInfo( - name=r.entry.name, - type=map_file_type(r.entry.type), - path=r.entry.path, - size=r.entry.size, - mode=r.entry.mode, - permissions=r.entry.permissions, - owner=r.entry.owner, - group=r.entry.group, - modified_time=r.entry.modified_time.ToDatetime(), - # Optional, we can't directly access symlink_target otherwise if will be "" instead of None - symlink_target=( - r.entry.symlink_target - if r.entry.HasField("symlink_target") - else None - ), - ) + return map_entry_info(r.entry) except Exception as e: raise _handle_filesystem_rpc_exception(e) @@ -597,6 +563,7 @@ async def watch_dir( request_timeout: Optional[float] = None, timeout: Optional[float] = 60, recursive: bool = False, + include_entry: bool = False, ) -> AsyncWatchHandle: """ Watch directory for filesystem events. @@ -608,6 +575,7 @@ async def watch_dir( :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 + :param include_entry: Include the `EntryInfo` of the affected entry in each event, when available. Requires envd 0.6.2 or later; ignored by older sandboxes :return: `AsyncWatchHandle` object for stopping watching directory """ @@ -618,7 +586,9 @@ async def watch_dir( ) events = self._rpc.awatch_dir( - filesystem_pb2.WatchDirRequest(path=path, recursive=recursive), + filesystem_pb2.WatchDirRequest( + path=path, recursive=recursive, include_entry=include_entry + ), request_timeout=self._connection_config.get_request_timeout( request_timeout ), diff --git a/packages/python-sdk/e2b/sandbox_async/filesystem/watch_handle.py b/packages/python-sdk/e2b/sandbox_async/filesystem/watch_handle.py index 6a32de50c3..a33d13fd15 100644 --- a/packages/python-sdk/e2b/sandbox_async/filesystem/watch_handle.py +++ b/packages/python-sdk/e2b/sandbox_async/filesystem/watch_handle.py @@ -5,6 +5,7 @@ from e2b.envd.rpc import handle_rpc_exception from e2b.envd.filesystem.filesystem_pb2 import WatchDirResponse +from e2b.sandbox.filesystem.filesystem import map_entry_info from e2b.sandbox.filesystem.watch_handle import FilesystemEvent, map_event_type from e2b.sandbox_async.utils import OutputHandler @@ -48,6 +49,11 @@ async def _iterate_events(self): yield FilesystemEvent( name=event.filesystem.name, type=event_type, + entry=( + map_entry_info(event.filesystem.entry) + if event.filesystem.HasField("entry") + else None + ), ) except Exception as e: raise handle_rpc_exception(e) diff --git a/packages/python-sdk/e2b/sandbox_sync/filesystem/filesystem.py b/packages/python-sdk/e2b/sandbox_sync/filesystem/filesystem.py index b145200e41..60f12f1895 100644 --- a/packages/python-sdk/e2b/sandbox_sync/filesystem/filesystem.py +++ b/packages/python-sdk/e2b/sandbox_sync/filesystem/filesystem.py @@ -33,6 +33,7 @@ EntryInfo, WriteEntry, WriteInfo, + map_entry_info, map_file_type, to_upload_body, ) @@ -371,28 +372,9 @@ def list( entries: List[EntryInfo] = [] for entry in res.entries: - event_type = map_file_type(entry.type) - - if event_type: - entries.append( - EntryInfo( - name=entry.name, - type=event_type, - path=entry.path, - size=entry.size, - mode=entry.mode, - permissions=entry.permissions, - owner=entry.owner, - group=entry.group, - modified_time=entry.modified_time.ToDatetime(), - # Optional, we can't directly access symlink_target otherwise if will be "" instead of None - symlink_target=( - entry.symlink_target - if entry.HasField("symlink_target") - else None - ), - ) - ) + # Skip entries with an unknown file type. + if map_file_type(entry.type): + entries.append(map_entry_info(entry)) return entries except Exception as e: @@ -453,23 +435,7 @@ def get_info( headers=authentication_header(self._envd_version, user), ) - return EntryInfo( - name=r.entry.name, - type=map_file_type(r.entry.type), - path=r.entry.path, - size=r.entry.size, - mode=r.entry.mode, - permissions=r.entry.permissions, - owner=r.entry.owner, - group=r.entry.group, - modified_time=r.entry.modified_time.ToDatetime(), - # Optional, we can't directly access symlink_target otherwise if will be "" instead of None - symlink_target=( - r.entry.symlink_target - if r.entry.HasField("symlink_target") - else None - ), - ) + return map_entry_info(r.entry) except Exception as e: raise _handle_filesystem_rpc_exception(e) @@ -526,23 +492,7 @@ def rename( headers=authentication_header(self._envd_version, user), ) - return EntryInfo( - name=r.entry.name, - type=map_file_type(r.entry.type), - path=r.entry.path, - size=r.entry.size, - mode=r.entry.mode, - permissions=r.entry.permissions, - owner=r.entry.owner, - group=r.entry.group, - modified_time=r.entry.modified_time.ToDatetime(), - # Optional, we can't directly access symlink_target otherwise if will be "" instead of None - symlink_target=( - r.entry.symlink_target - if r.entry.HasField("symlink_target") - else None - ), - ) + return map_entry_info(r.entry) except Exception as e: raise _handle_filesystem_rpc_exception(e) @@ -583,6 +533,7 @@ def watch_dir( user: Optional[Username] = None, request_timeout: Optional[float] = None, recursive: bool = False, + include_entry: bool = False, ) -> WatchHandle: """ Watch directory for filesystem events. @@ -591,6 +542,7 @@ def watch_dir( :param user: Run the operation as this user :param request_timeout: Timeout for the request in **seconds** :param recursive: Watch directory recursively + :param include_entry: Include the `EntryInfo` of the affected entry in each event, when available. Requires envd 0.6.2 or later; ignored by older sandboxes :return: `WatchHandle` object for stopping watching directory """ @@ -602,7 +554,9 @@ def watch_dir( try: r = self._rpc.create_watcher( - filesystem_pb2.CreateWatcherRequest(path=path, recursive=recursive), + filesystem_pb2.CreateWatcherRequest( + path=path, recursive=recursive, include_entry=include_entry + ), request_timeout=self._connection_config.get_request_timeout( request_timeout ), diff --git a/packages/python-sdk/e2b/sandbox_sync/filesystem/watch_handle.py b/packages/python-sdk/e2b/sandbox_sync/filesystem/watch_handle.py index bbf531c57d..dfc3cab4c1 100644 --- a/packages/python-sdk/e2b/sandbox_sync/filesystem/watch_handle.py +++ b/packages/python-sdk/e2b/sandbox_sync/filesystem/watch_handle.py @@ -7,6 +7,7 @@ RemoveWatcherRequest, ) from e2b.envd.rpc import handle_rpc_exception +from e2b.sandbox.filesystem.filesystem import map_entry_info from e2b.sandbox.filesystem.watch_handle import FilesystemEvent, map_event_type @@ -63,6 +64,11 @@ def get_new_events(self) -> List[FilesystemEvent]: FilesystemEvent( name=event.name, type=event_type, + entry=( + map_entry_info(event.entry) + if event.HasField("entry") + else None + ), ) ) 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 7ee0e54bc2..bf2730a17b 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 @@ -7,10 +7,46 @@ AsyncSandbox, FilesystemEvent, FilesystemEventType, + FileType, SandboxException, ) +async def test_watch_directory_changes_with_entry_info(async_sandbox: AsyncSandbox): + dirname = "test_watch_dir_entry" + filename = "test_watch.txt" + content = "This file will be watched." + new_content = "This file has been modified." + + await async_sandbox.files.make_dir(dirname) + await async_sandbox.files.write(f"{dirname}/{filename}", content) + + event_triggered = Event() + received: list[FilesystemEvent] = [] + + def handle_event(e: FilesystemEvent): + if e.type == FilesystemEventType.WRITE and e.name == filename: + received.append(e) + event_triggered.set() + + handle = await async_sandbox.files.watch_dir( + dirname, on_event=handle_event, include_entry=True + ) + + await async_sandbox.files.write(f"{dirname}/{filename}", new_content) + + await event_triggered.wait() + + write_event = received[0] + # The entry is populated best-effort for events where the path still exists. + assert write_event.entry is not None + assert write_event.entry.name == filename + assert write_event.entry.path == f"/home/user/{dirname}/{filename}" + assert write_event.entry.type == FileType.FILE + + await handle.stop() + + async def test_watch_directory_changes(async_sandbox: AsyncSandbox): dirname = "test_watch_dir" filename = "test_watch.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 03a5d6c661..11900dac66 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 @@ -1,6 +1,43 @@ import pytest -from e2b import FileNotFoundException, FilesystemEventType, Sandbox, SandboxException +from e2b import ( + FileNotFoundException, + FileType, + FilesystemEventType, + Sandbox, + SandboxException, +) + + +def test_watch_directory_changes_with_entry_info(sandbox: Sandbox): + dirname = "test_watch_dir_entry" + filename = "test_watch.txt" + content = "This file will be watched." + new_content = "This file has been modified." + + sandbox.files.make_dir(dirname) + sandbox.files.write(f"{dirname}/{filename}", content) + + handle = sandbox.files.watch_dir(dirname, include_entry=True) + sandbox.files.write(f"{dirname}/{filename}", new_content) + + events = handle.get_new_events() + write_event = None + for event in events: + if event.type == FilesystemEventType.WRITE and event.name == filename: + write_event = event + break + + assert write_event is not None, ( + f"Expected WRITE event for {filename}, but got events: {events}" + ) + # The entry is populated best-effort for events where the path still exists. + assert write_event.entry is not None + assert write_event.entry.name == filename + assert write_event.entry.path == f"/home/user/{dirname}/{filename}" + assert write_event.entry.type == FileType.FILE + + handle.stop() def test_watch_directory_changes(sandbox: Sandbox): diff --git a/spec/envd/filesystem/filesystem.proto b/spec/envd/filesystem/filesystem.proto index ca9aeb12df..15f84521f1 100644 --- a/spec/envd/filesystem/filesystem.proto +++ b/spec/envd/filesystem/filesystem.proto @@ -82,11 +82,17 @@ message ListDirResponse { message WatchDirRequest { string path = 1; bool recursive = 2; + // If true, each FilesystemEvent includes the EntryInfo of the affected entry, when available. + bool include_entry = 3; } message FilesystemEvent { string name = 1; EventType type = 2; + // Info of the entry that triggered the event. Only populated when include_entry + // was requested and the entry could be stat-ed (e.g. not set for remove/rename-away + // events, where the entry no longer exists at this path). + optional EntryInfo entry = 3; } message WatchDirResponse { @@ -104,6 +110,8 @@ message WatchDirResponse { message CreateWatcherRequest { string path = 1; bool recursive = 2; + // If true, each FilesystemEvent includes the EntryInfo of the affected entry, when available. + bool include_entry = 3; } message CreateWatcherResponse { From 551fc362735a7f3d1480eabff7a6659959310d75 Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Fri, 5 Jun 2026 12:35:17 +0200 Subject: [PATCH 2/2] refactor(python-sdk): use map_entry_info in async get_info The lone remaining inline EntryInfo construction; brings the async get_info call site in line with the other list/get_info/rename sites across the JS and Python SDKs. Co-Authored-By: Claude Opus 4.8 --- .../e2b/sandbox_async/filesystem/filesystem.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/packages/python-sdk/e2b/sandbox_async/filesystem/filesystem.py b/packages/python-sdk/e2b/sandbox_async/filesystem/filesystem.py index 7d640c35ac..46031b2bbd 100644 --- a/packages/python-sdk/e2b/sandbox_async/filesystem/filesystem.py +++ b/packages/python-sdk/e2b/sandbox_async/filesystem/filesystem.py @@ -447,22 +447,7 @@ async def get_info( headers=authentication_header(self._envd_version, user), ) - return EntryInfo( - name=r.entry.name, - type=map_file_type(r.entry.type), - path=r.entry.path, - size=r.entry.size, - mode=r.entry.mode, - permissions=r.entry.permissions, - owner=r.entry.owner, - group=r.entry.group, - modified_time=r.entry.modified_time.ToDatetime(), - symlink_target=( - r.entry.symlink_target - if r.entry.HasField("symlink_target") - else None - ), - ) + return map_entry_info(r.entry) except Exception as e: raise _handle_filesystem_rpc_exception(e)