diff --git a/.changeset/proud-ants-flash.md b/.changeset/proud-ants-flash.md
new file mode 100644
index 0000000000..435d262c61
--- /dev/null
+++ b/.changeset/proud-ants-flash.md
@@ -0,0 +1,6 @@
+---
+'@e2b/python-sdk': minor
+'e2b': minor
+---
+
+added getInfo/get_info method for file information
diff --git a/apps/web/src/app/(docs)/docs/filesystem/info/page.mdx b/apps/web/src/app/(docs)/docs/filesystem/info/page.mdx
new file mode 100644
index 0000000000..70354ba7b7
--- /dev/null
+++ b/apps/web/src/app/(docs)/docs/filesystem/info/page.mdx
@@ -0,0 +1,113 @@
+# Get information about a file or directory
+
+You can get information about a file or directory using the `files.getInfo()` / `files.get_info()` methods. Information such as file name, type, and path is returned.
+
+### Getting information about a file
+
+
+```js
+import { Sandbox } from '@e2b/code-interpreter'
+
+const sandbox = await Sandbox.create()
+
+// Create a new file
+await sandbox.files.write('test_file.txt', 'Hello, world!')
+
+// Get information about the file
+const info = await sandbox.files.getInfo('test_file.txt')
+
+console.log(info)
+// {
+// name: 'test_file.txt',
+// type: 'file',
+// path: '/home/user/test_file.txt',
+// size: 13,
+// mode: 0o644,
+// permissions: '-rw-r--r--',
+// owner: 'user',
+// group: 'user',
+// modifiedTime: '2025-05-26T12:00:00.000Z',
+// symlinkTarget: null
+// }
+```
+```python
+from e2b_code_interpreter import Sandbox
+
+sandbox = Sandbox()
+
+# Create a new file
+sandbox.files.write('test_file', 'Hello, world!')
+
+# Get information about the file
+info = sandbox.files.get_info('test_file')
+
+print(info)
+# EntryInfo(
+# name='test_file.txt',
+# type=,
+# path='/home/user/test_file.txt',
+# size=13,
+# mode=0o644,
+# permissions='-rw-r--r--',
+# owner='user',
+# group='user',
+# modified_time='2025-05-26T12:00:00.000Z',
+# symlink_target=None
+# )
+```
+
+
+### Getting information about a directory
+
+
+```js
+import { Sandbox } from '@e2b/code-interpreter'
+
+const sandbox = await Sandbox.create()
+
+// Create a new directory
+await sandbox.files.makeDir('test_dir')
+
+// Get information about the directory
+const info = await sandbox.files.getInfo('test_dir')
+
+console.log(info)
+// {
+// name: 'test_dir',
+// type: 'dir',
+// path: '/home/user/test_dir',
+// size: 0,
+// mode: 0o755,
+// permissions: 'drwxr-xr-x',
+// owner: 'user',
+// group: 'user',
+// modifiedTime: '2025-05-26T12:00:00.000Z',
+// symlinkTarget: null
+// }
+```
+```python
+from e2b_code_interpreter import Sandbox
+
+sandbox = Sandbox()
+
+# Create a new directory
+sandbox.files.make_dir('test_dir')
+
+# Get information about the directory
+info = sandbox.files.get_info('test_dir')
+
+print(info)
+# EntryInfo(
+# name='test_dir',
+# type=,
+# path='/home/user/test_dir',
+# size=0,
+# mode=0o755,
+# permissions='drwxr-xr-x',
+# owner='user',
+# group='user',
+# modified_time='2025-05-26T12:00:00.000Z',
+# symlink_target=None
+# )
+```
+
diff --git a/apps/web/src/components/Navigation/routes.tsx b/apps/web/src/components/Navigation/routes.tsx
index a432a66a87..f5578e8c5e 100644
--- a/apps/web/src/components/Navigation/routes.tsx
+++ b/apps/web/src/components/Navigation/routes.tsx
@@ -359,6 +359,10 @@ export const docRoutes: NavGroup[] = [
title: 'Read & write',
href: '/docs/filesystem/read-write',
},
+ {
+ title: 'Get info about a file or directory',
+ href: '/docs/filesystem/info',
+ },
{
title: 'Watch directory for changes',
href: '/docs/filesystem/watch',
diff --git a/packages/js-sdk/package.json b/packages/js-sdk/package.json
index 9bea6f75cd..c532389c0c 100644
--- a/packages/js-sdk/package.json
+++ b/packages/js-sdk/package.json
@@ -30,6 +30,7 @@
"test": "vitest run",
"generate": "npm-run-all generate:*",
"generate:api": "python ./../../spec/remove_extra_tags.py sandboxes templates auth && openapi-typescript ../../spec/openapi_generated.yml -x api_key --array-length --alphabetize --output src/api/schema.gen.ts",
+ "generate:envd": "cd ../../spec/envd && buf generate --template buf-js.gen.yaml\n",
"generate:envd-api": "openapi-typescript ../../spec/envd/envd.yaml -x api_key --array-length --alphabetize --output src/envd/schema.gen.ts",
"generate-ref": "./scripts/generate_sdk_ref.sh",
"check-deps": "knip",
diff --git a/packages/js-sdk/src/envd/filesystem/filesystem_pb.ts b/packages/js-sdk/src/envd/filesystem/filesystem_pb.ts
index 4b879bd593..ea9798d1fc 100644
--- a/packages/js-sdk/src/envd/filesystem/filesystem_pb.ts
+++ b/packages/js-sdk/src/envd/filesystem/filesystem_pb.ts
@@ -4,13 +4,15 @@
import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
import { enumDesc, fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2";
+import type { Timestamp } from "@bufbuild/protobuf/wkt";
+import { file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt";
import type { Message } from "@bufbuild/protobuf";
/**
* Describes the file filesystem/filesystem.proto.
*/
export const file_filesystem_filesystem: GenFile = /*@__PURE__*/
- fileDesc("ChtmaWxlc3lzdGVtL2ZpbGVzeXN0ZW0ucHJvdG8SCmZpbGVzeXN0ZW0iMgoLTW92ZVJlcXVlc3QSDgoGc291cmNlGAEgASgJEhMKC2Rlc3RpbmF0aW9uGAIgASgJIjQKDE1vdmVSZXNwb25zZRIkCgVlbnRyeRgBIAEoCzIVLmZpbGVzeXN0ZW0uRW50cnlJbmZvIh4KDk1ha2VEaXJSZXF1ZXN0EgwKBHBhdGgYASABKAkiNwoPTWFrZURpclJlc3BvbnNlEiQKBWVudHJ5GAEgASgLMhUuZmlsZXN5c3RlbS5FbnRyeUluZm8iHQoNUmVtb3ZlUmVxdWVzdBIMCgRwYXRoGAEgASgJIhAKDlJlbW92ZVJlc3BvbnNlIhsKC1N0YXRSZXF1ZXN0EgwKBHBhdGgYASABKAkiNAoMU3RhdFJlc3BvbnNlEiQKBWVudHJ5GAEgASgLMhUuZmlsZXN5c3RlbS5FbnRyeUluZm8iSwoJRW50cnlJbmZvEgwKBG5hbWUYASABKAkSIgoEdHlwZRgCIAEoDjIULmZpbGVzeXN0ZW0uRmlsZVR5cGUSDAoEcGF0aBgDIAEoCSItCg5MaXN0RGlyUmVxdWVzdBIMCgRwYXRoGAEgASgJEg0KBWRlcHRoGAIgASgNIjkKD0xpc3REaXJSZXNwb25zZRImCgdlbnRyaWVzGAEgAygLMhUuZmlsZXN5c3RlbS5FbnRyeUluZm8iMgoPV2F0Y2hEaXJSZXF1ZXN0EgwKBHBhdGgYASABKAkSEQoJcmVjdXJzaXZlGAIgASgIIkQKD0ZpbGVzeXN0ZW1FdmVudBIMCgRuYW1lGAEgASgJEiMKBHR5cGUYAiABKA4yFS5maWxlc3lzdGVtLkV2ZW50VHlwZSLgAQoQV2F0Y2hEaXJSZXNwb25zZRI4CgVzdGFydBgBIAEoCzInLmZpbGVzeXN0ZW0uV2F0Y2hEaXJSZXNwb25zZS5TdGFydEV2ZW50SAASMQoKZmlsZXN5c3RlbRgCIAEoCzIbLmZpbGVzeXN0ZW0uRmlsZXN5c3RlbUV2ZW50SAASOwoJa2VlcGFsaXZlGAMgASgLMiYuZmlsZXN5c3RlbS5XYXRjaERpclJlc3BvbnNlLktlZXBBbGl2ZUgAGgwKClN0YXJ0RXZlbnQaCwoJS2VlcEFsaXZlQgcKBWV2ZW50IjcKFENyZWF0ZVdhdGNoZXJSZXF1ZXN0EgwKBHBhdGgYASABKAkSEQoJcmVjdXJzaXZlGAIgASgIIisKFUNyZWF0ZVdhdGNoZXJSZXNwb25zZRISCgp3YXRjaGVyX2lkGAEgASgJIi0KF0dldFdhdGNoZXJFdmVudHNSZXF1ZXN0EhIKCndhdGNoZXJfaWQYASABKAkiRwoYR2V0V2F0Y2hlckV2ZW50c1Jlc3BvbnNlEisKBmV2ZW50cxgBIAMoCzIbLmZpbGVzeXN0ZW0uRmlsZXN5c3RlbUV2ZW50IioKFFJlbW92ZVdhdGNoZXJSZXF1ZXN0EhIKCndhdGNoZXJfaWQYASABKAkiFwoVUmVtb3ZlV2F0Y2hlclJlc3BvbnNlKlIKCEZpbGVUeXBlEhkKFUZJTEVfVFlQRV9VTlNQRUNJRklFRBAAEhIKDkZJTEVfVFlQRV9GSUxFEAESFwoTRklMRV9UWVBFX0RJUkVDVE9SWRACKpgBCglFdmVudFR5cGUSGgoWRVZFTlRfVFlQRV9VTlNQRUNJRklFRBAAEhUKEUVWRU5UX1RZUEVfQ1JFQVRFEAESFAoQRVZFTlRfVFlQRV9XUklURRACEhUKEUVWRU5UX1RZUEVfUkVNT1ZFEAMSFQoRRVZFTlRfVFlQRV9SRU5BTUUQBBIUChBFVkVOVF9UWVBFX0NITU9EEAUynwUKCkZpbGVzeXN0ZW0SOQoEU3RhdBIXLmZpbGVzeXN0ZW0uU3RhdFJlcXVlc3QaGC5maWxlc3lzdGVtLlN0YXRSZXNwb25zZRJCCgdNYWtlRGlyEhouZmlsZXN5c3RlbS5NYWtlRGlyUmVxdWVzdBobLmZpbGVzeXN0ZW0uTWFrZURpclJlc3BvbnNlEjkKBE1vdmUSFy5maWxlc3lzdGVtLk1vdmVSZXF1ZXN0GhguZmlsZXN5c3RlbS5Nb3ZlUmVzcG9uc2USQgoHTGlzdERpchIaLmZpbGVzeXN0ZW0uTGlzdERpclJlcXVlc3QaGy5maWxlc3lzdGVtLkxpc3REaXJSZXNwb25zZRI/CgZSZW1vdmUSGS5maWxlc3lzdGVtLlJlbW92ZVJlcXVlc3QaGi5maWxlc3lzdGVtLlJlbW92ZVJlc3BvbnNlEkcKCFdhdGNoRGlyEhsuZmlsZXN5c3RlbS5XYXRjaERpclJlcXVlc3QaHC5maWxlc3lzdGVtLldhdGNoRGlyUmVzcG9uc2UwARJUCg1DcmVhdGVXYXRjaGVyEiAuZmlsZXN5c3RlbS5DcmVhdGVXYXRjaGVyUmVxdWVzdBohLmZpbGVzeXN0ZW0uQ3JlYXRlV2F0Y2hlclJlc3BvbnNlEl0KEEdldFdhdGNoZXJFdmVudHMSIy5maWxlc3lzdGVtLkdldFdhdGNoZXJFdmVudHNSZXF1ZXN0GiQuZmlsZXN5c3RlbS5HZXRXYXRjaGVyRXZlbnRzUmVzcG9uc2USVAoNUmVtb3ZlV2F0Y2hlchIgLmZpbGVzeXN0ZW0uUmVtb3ZlV2F0Y2hlclJlcXVlc3QaIS5maWxlc3lzdGVtLlJlbW92ZVdhdGNoZXJSZXNwb25zZUJpCg5jb20uZmlsZXN5c3RlbUIPRmlsZXN5c3RlbVByb3RvUAGiAgNGWFiqAgpGaWxlc3lzdGVtygIKRmlsZXN5c3RlbeICFkZpbGVzeXN0ZW1cR1BCTWV0YWRhdGHqAgpGaWxlc3lzdGVtYgZwcm90bzM");
+ fileDesc("ChtmaWxlc3lzdGVtL2ZpbGVzeXN0ZW0ucHJvdG8SCmZpbGVzeXN0ZW0iMgoLTW92ZVJlcXVlc3QSDgoGc291cmNlGAEgASgJEhMKC2Rlc3RpbmF0aW9uGAIgASgJIjQKDE1vdmVSZXNwb25zZRIkCgVlbnRyeRgBIAEoCzIVLmZpbGVzeXN0ZW0uRW50cnlJbmZvIh4KDk1ha2VEaXJSZXF1ZXN0EgwKBHBhdGgYASABKAkiNwoPTWFrZURpclJlc3BvbnNlEiQKBWVudHJ5GAEgASgLMhUuZmlsZXN5c3RlbS5FbnRyeUluZm8iHQoNUmVtb3ZlUmVxdWVzdBIMCgRwYXRoGAEgASgJIhAKDlJlbW92ZVJlc3BvbnNlIhsKC1N0YXRSZXF1ZXN0EgwKBHBhdGgYASABKAkiNAoMU3RhdFJlc3BvbnNlEiQKBWVudHJ5GAEgASgLMhUuZmlsZXN5c3RlbS5FbnRyeUluZm8i/QEKCUVudHJ5SW5mbxIMCgRuYW1lGAEgASgJEiIKBHR5cGUYAiABKA4yFC5maWxlc3lzdGVtLkZpbGVUeXBlEgwKBHBhdGgYAyABKAkSDAoEc2l6ZRgEIAEoAxIMCgRtb2RlGAUgASgNEhMKC3Blcm1pc3Npb25zGAYgASgJEg0KBW93bmVyGAcgASgJEg0KBWdyb3VwGAggASgJEjEKDW1vZGlmaWVkX3RpbWUYCSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhsKDnN5bWxpbmtfdGFyZ2V0GAogASgJSACIAQFCEQoPX3N5bWxpbmtfdGFyZ2V0Ii0KDkxpc3REaXJSZXF1ZXN0EgwKBHBhdGgYASABKAkSDQoFZGVwdGgYAiABKA0iOQoPTGlzdERpclJlc3BvbnNlEiYKB2VudHJpZXMYASADKAsyFS5maWxlc3lzdGVtLkVudHJ5SW5mbyIyCg9XYXRjaERpclJlcXVlc3QSDAoEcGF0aBgBIAEoCRIRCglyZWN1cnNpdmUYAiABKAgiRAoPRmlsZXN5c3RlbUV2ZW50EgwKBG5hbWUYASABKAkSIwoEdHlwZRgCIAEoDjIVLmZpbGVzeXN0ZW0uRXZlbnRUeXBlIuABChBXYXRjaERpclJlc3BvbnNlEjgKBXN0YXJ0GAEgASgLMicuZmlsZXN5c3RlbS5XYXRjaERpclJlc3BvbnNlLlN0YXJ0RXZlbnRIABIxCgpmaWxlc3lzdGVtGAIgASgLMhsuZmlsZXN5c3RlbS5GaWxlc3lzdGVtRXZlbnRIABI7CglrZWVwYWxpdmUYAyABKAsyJi5maWxlc3lzdGVtLldhdGNoRGlyUmVzcG9uc2UuS2VlcEFsaXZlSAAaDAoKU3RhcnRFdmVudBoLCglLZWVwQWxpdmVCBwoFZXZlbnQiNwoUQ3JlYXRlV2F0Y2hlclJlcXVlc3QSDAoEcGF0aBgBIAEoCRIRCglyZWN1cnNpdmUYAiABKAgiKwoVQ3JlYXRlV2F0Y2hlclJlc3BvbnNlEhIKCndhdGNoZXJfaWQYASABKAkiLQoXR2V0V2F0Y2hlckV2ZW50c1JlcXVlc3QSEgoKd2F0Y2hlcl9pZBgBIAEoCSJHChhHZXRXYXRjaGVyRXZlbnRzUmVzcG9uc2USKwoGZXZlbnRzGAEgAygLMhsuZmlsZXN5c3RlbS5GaWxlc3lzdGVtRXZlbnQiKgoUUmVtb3ZlV2F0Y2hlclJlcXVlc3QSEgoKd2F0Y2hlcl9pZBgBIAEoCSIXChVSZW1vdmVXYXRjaGVyUmVzcG9uc2UqUgoIRmlsZVR5cGUSGQoVRklMRV9UWVBFX1VOU1BFQ0lGSUVEEAASEgoORklMRV9UWVBFX0ZJTEUQARIXChNGSUxFX1RZUEVfRElSRUNUT1JZEAIqmAEKCUV2ZW50VHlwZRIaChZFVkVOVF9UWVBFX1VOU1BFQ0lGSUVEEAASFQoRRVZFTlRfVFlQRV9DUkVBVEUQARIUChBFVkVOVF9UWVBFX1dSSVRFEAISFQoRRVZFTlRfVFlQRV9SRU1PVkUQAxIVChFFVkVOVF9UWVBFX1JFTkFNRRAEEhQKEEVWRU5UX1RZUEVfQ0hNT0QQBTKfBQoKRmlsZXN5c3RlbRI5CgRTdGF0EhcuZmlsZXN5c3RlbS5TdGF0UmVxdWVzdBoYLmZpbGVzeXN0ZW0uU3RhdFJlc3BvbnNlEkIKB01ha2VEaXISGi5maWxlc3lzdGVtLk1ha2VEaXJSZXF1ZXN0GhsuZmlsZXN5c3RlbS5NYWtlRGlyUmVzcG9uc2USOQoETW92ZRIXLmZpbGVzeXN0ZW0uTW92ZVJlcXVlc3QaGC5maWxlc3lzdGVtLk1vdmVSZXNwb25zZRJCCgdMaXN0RGlyEhouZmlsZXN5c3RlbS5MaXN0RGlyUmVxdWVzdBobLmZpbGVzeXN0ZW0uTGlzdERpclJlc3BvbnNlEj8KBlJlbW92ZRIZLmZpbGVzeXN0ZW0uUmVtb3ZlUmVxdWVzdBoaLmZpbGVzeXN0ZW0uUmVtb3ZlUmVzcG9uc2USRwoIV2F0Y2hEaXISGy5maWxlc3lzdGVtLldhdGNoRGlyUmVxdWVzdBocLmZpbGVzeXN0ZW0uV2F0Y2hEaXJSZXNwb25zZTABElQKDUNyZWF0ZVdhdGNoZXISIC5maWxlc3lzdGVtLkNyZWF0ZVdhdGNoZXJSZXF1ZXN0GiEuZmlsZXN5c3RlbS5DcmVhdGVXYXRjaGVyUmVzcG9uc2USXQoQR2V0V2F0Y2hlckV2ZW50cxIjLmZpbGVzeXN0ZW0uR2V0V2F0Y2hlckV2ZW50c1JlcXVlc3QaJC5maWxlc3lzdGVtLkdldFdhdGNoZXJFdmVudHNSZXNwb25zZRJUCg1SZW1vdmVXYXRjaGVyEiAuZmlsZXN5c3RlbS5SZW1vdmVXYXRjaGVyUmVxdWVzdBohLmZpbGVzeXN0ZW0uUmVtb3ZlV2F0Y2hlclJlc3BvbnNlQmkKDmNvbS5maWxlc3lzdGVtQg9GaWxlc3lzdGVtUHJvdG9QAaICA0ZYWKoCCkZpbGVzeXN0ZW3KAgpGaWxlc3lzdGVt4gIWRmlsZXN5c3RlbVxHUEJNZXRhZGF0YeoCCkZpbGVzeXN0ZW1iBnByb3RvMw", [file_google_protobuf_timestamp]);
/**
* @generated from message filesystem.MoveRequest
@@ -167,6 +169,43 @@ export type EntryInfo = Message<"filesystem.EntryInfo"> & {
* @generated from field: string path = 3;
*/
path: string;
+
+ /**
+ * @generated from field: int64 size = 4;
+ */
+ size: bigint;
+
+ /**
+ * @generated from field: uint32 mode = 5;
+ */
+ mode: number;
+
+ /**
+ * @generated from field: string permissions = 6;
+ */
+ permissions: string;
+
+ /**
+ * @generated from field: string owner = 7;
+ */
+ owner: string;
+
+ /**
+ * @generated from field: string group = 8;
+ */
+ group: string;
+
+ /**
+ * @generated from field: google.protobuf.Timestamp modified_time = 9;
+ */
+ modifiedTime?: Timestamp;
+
+ /**
+ * If the entry is a symlink, this field contains the target of the symlink.
+ *
+ * @generated from field: optional string symlink_target = 10;
+ */
+ symlinkTarget?: string;
};
/**
diff --git a/packages/js-sdk/src/index.ts b/packages/js-sdk/src/index.ts
index 7aa151a7c7..a0acdbf209 100644
--- a/packages/js-sdk/src/index.ts
+++ b/packages/js-sdk/src/index.ts
@@ -17,9 +17,12 @@ export type { Logger } from './logs'
export { getSignature } from './sandbox/signature'
export { FileType } from './sandbox/filesystem'
-export type { EntryInfo, Filesystem } from './sandbox/filesystem'
+export type { WriteInfo, EntryInfo, Filesystem } from './sandbox/filesystem'
export { FilesystemEventType } from './sandbox/filesystem/watchHandle'
-export type { FilesystemEvent, WatchHandle } from './sandbox/filesystem/watchHandle'
+export type {
+ FilesystemEvent,
+ WatchHandle,
+} from './sandbox/filesystem/watchHandle'
export { CommandExitError } from './sandbox/commands/commandHandle'
export type {
diff --git a/packages/js-sdk/src/sandbox/filesystem/index.ts b/packages/js-sdk/src/sandbox/filesystem/index.ts
index 3dbd74ac62..9fabec4c51 100644
--- a/packages/js-sdk/src/sandbox/filesystem/index.ts
+++ b/packages/js-sdk/src/sandbox/filesystem/index.ts
@@ -28,11 +28,12 @@ import { FilesystemEvent, WatchHandle } from './watchHandle'
import { compareVersions } from 'compare-versions'
import { InvalidArgumentError, TemplateError } from '../../errors'
import { ENVD_VERSION_RECURSIVE_WATCH } from '../../envd/versions'
+import type { Timestamp } from '@bufbuild/protobuf/wkt'
/**
* Sandbox filesystem object information.
*/
-export interface EntryInfo {
+export interface WriteInfo {
/**
* Name of the filesystem object.
*/
@@ -47,6 +48,43 @@ export interface EntryInfo {
path: string
}
+export interface EntryInfo extends WriteInfo {
+ /**
+ * Size of the filesystem object in bytes.
+ */
+ size: number
+
+ /**
+ * File mode and permission bits.
+ */
+ mode: number
+
+ /**
+ * String representation of file permissions (e.g. 'rwxr-xr-x').
+ */
+ permissions: string
+
+ /**
+ * Owner of the filesystem object.
+ */
+ owner: string
+
+ /**
+ * Group owner of the filesystem object.
+ */
+ group: string
+
+ /**
+ * Last modification time of the filesystem object.
+ */
+ modifiedTime?: Date
+
+ /**
+ * If the filesystem object is a symlink, this is the target of the symlink.
+ */
+ symlinkTarget?: string
+}
+
/**
* Sandbox filesystem object type.
*/
@@ -75,6 +113,15 @@ function mapFileType(fileType: FsFileType) {
}
}
+function mapModifiedTime(modifiedTime: Timestamp | undefined) {
+ if (!modifiedTime) return undefined
+
+ return new Date(
+ Number(modifiedTime.seconds) * 1000 +
+ Math.floor(modifiedTime.nanos / 1_000_000)
+ )
+}
+
/**
* Options for the sandbox filesystem operations.
*/
@@ -248,11 +295,11 @@ export class Filesystem {
path: string,
data: string | ArrayBuffer | Blob | ReadableStream,
opts?: FilesystemRequestOpts
- ): Promise
+ ): Promise
async write(
files: WriteEntry[],
opts?: FilesystemRequestOpts
- ): Promise
+ ): Promise
async write(
pathOrFiles: string | WriteEntry[],
dataOrOpts?:
@@ -262,7 +309,7 @@ export class Filesystem {
| ReadableStream
| FilesystemRequestOpts,
opts?: FilesystemRequestOpts
- ): Promise {
+ ): Promise {
if (typeof pathOrFiles !== 'string' && !Array.isArray(pathOrFiles)) {
throw new Error('Path or files are required')
}
@@ -294,7 +341,7 @@ export class Filesystem {
writeFiles: pathOrFiles as WriteEntry[],
}
- if (writeFiles.length === 0) return [] as EntryInfo[]
+ if (writeFiles.length === 0) return [] as WriteInfo[]
const blobs = await Promise.all(
writeFiles.map((f) => new Response(f.data).blob())
@@ -330,7 +377,7 @@ export class Filesystem {
throw err
}
- const files = res.data as EntryInfo[]
+ const files = res.data as WriteInfo[]
if (!files) {
throw new Error('Expected to receive information about written file')
}
@@ -373,6 +420,13 @@ export class Filesystem {
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,
})
}
}
@@ -448,6 +502,13 @@ export class Filesystem {
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,
}
} catch (err) {
throw handleRpcError(err)
@@ -504,6 +565,47 @@ export class Filesystem {
}
}
+ /**
+ * Get information about a file or directory.
+ *
+ * @param path path to a file or directory.
+ * @param opts connection options.
+ *
+ * @returns information about the file or directory like name, type, and path.
+ */
+ async getInfo(
+ path: string,
+ opts?: FilesystemRequestOpts
+ ): Promise {
+ try {
+ const res = await this.rpc.stat(
+ { path },
+ { headers: authenticationHeader(opts?.user) }
+ )
+
+ if (!res.entry) {
+ throw new Error(
+ 'Expected to receive information about the file or directory'
+ )
+ }
+
+ 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,
+ }
+ } catch (err) {
+ throw handleRpcError(err)
+ }
+ }
+
/**
* Start watching a directory for filesystem events.
*
diff --git a/packages/js-sdk/tests/sandbox/files/info.test.ts b/packages/js-sdk/tests/sandbox/files/info.test.ts
new file mode 100644
index 0000000000..ee91ed7e06
--- /dev/null
+++ b/packages/js-sdk/tests/sandbox/files/info.test.ts
@@ -0,0 +1,68 @@
+import { assert } from 'vitest'
+import { expect } from 'vitest'
+import { NotFoundError } from '../../../src/errors.js'
+
+import { sandboxTest } from '../../setup.js'
+
+sandboxTest('get info of a file', async ({ sandbox }) => {
+ const filename = 'test_file.txt'
+
+ await sandbox.files.write(filename, 'test')
+ const info = await sandbox.files.getInfo(filename)
+ const { stdout: currentPath } = await sandbox.commands.run('pwd')
+
+ assert.equal(info.name, filename)
+ assert.equal(info.type, 'file')
+ assert.equal(info.path, currentPath.trim() + '/' + filename)
+ assert.equal(info.size, 4)
+ assert.equal(info.mode, 0o644)
+ assert.equal(info.permissions, '-rw-r--r--')
+ assert.equal(info.owner, 'user')
+ assert.equal(info.group, 'user')
+ assert.property(info, 'modifiedTime')
+})
+
+sandboxTest('get info of a file that does not exist', async ({ sandbox }) => {
+ const filename = 'test_does_not_exist.txt'
+ await expect(sandbox.files.getInfo(filename)).rejects.toThrow(NotFoundError)
+})
+
+sandboxTest('get info of a directory', async ({ sandbox }) => {
+ const dirname = 'test_dir'
+
+ await sandbox.files.makeDir(dirname)
+ const info = await sandbox.files.getInfo(dirname)
+ const { stdout: currentPath } = await sandbox.commands.run('pwd')
+
+ assert.equal(info.name, dirname)
+ assert.equal(info.type, 'dir')
+ assert.equal(info.path, currentPath.trim() + '/' + dirname)
+ assert.isAbove(info.size, 0)
+ assert.equal(info.mode, 0o755)
+ assert.equal(info.permissions, 'drwxr-xr-x')
+ assert.equal(info.owner, 'user')
+ assert.equal(info.group, 'user')
+ assert.property(info, 'modifiedTime')
+})
+
+sandboxTest(
+ 'get info of a directory that does not exist',
+ async ({ sandbox }) => {
+ const dirname = 'test_does_not_exist_dir'
+
+ await expect(sandbox.files.getInfo(dirname)).rejects.toThrow(NotFoundError)
+ }
+)
+
+sandboxTest('get info of a symlink', async ({ sandbox }) => {
+ const filename = 'test_file.txt'
+
+ await sandbox.files.write(filename, 'test')
+ const symlinkName = 'test_symlink.txt'
+ await sandbox.commands.run(`ln -s ${filename} ${symlinkName}`)
+
+ const info = await sandbox.files.getInfo(symlinkName)
+ const { stdout: currentPath } = await sandbox.commands.run('pwd')
+ assert.equal(info.name, symlinkName)
+ assert.equal(info.symlinkTarget, currentPath.trim() + '/' + filename)
+})
diff --git a/packages/js-sdk/tests/sandbox/files/list.test.ts b/packages/js-sdk/tests/sandbox/files/list.test.ts
index 2f9b649ea8..fbadab62a6 100644
--- a/packages/js-sdk/tests/sandbox/files/list.test.ts
+++ b/packages/js-sdk/tests/sandbox/files/list.test.ts
@@ -119,3 +119,88 @@ sandboxTest('list directory with invalid depth', async ({ sandbox }) => {
)
}
})
+
+sandboxTest('file entry details', async ({ sandbox }) => {
+ const testDir = 'test-file-entry'
+ const filePath = `${testDir}/test.txt`
+ const content = 'Hello, World!'
+
+ await sandbox.files.makeDir(testDir)
+ await sandbox.files.write(filePath, content)
+
+ const files = await sandbox.files.list(testDir, { depth: 1 })
+ assert.equal(files.length, 1)
+
+ const fileEntry = files[0]
+ assert.equal(fileEntry.name, 'test.txt')
+ assert.equal(fileEntry.path, `/home/user/${filePath}`)
+ assert.equal(fileEntry.type, 'file')
+ assert.equal(fileEntry.mode, 0o644)
+ assert.equal(fileEntry.permissions, '-rw-r--r--')
+ assert.equal(fileEntry.owner, 'user')
+ assert.equal(fileEntry.group, 'user')
+ assert.equal(fileEntry.size, content.length)
+ assert.ok(fileEntry.modifiedTime)
+ assert.isUndefined(fileEntry.symlinkTarget)
+})
+
+sandboxTest('directory entry details', async ({ sandbox }) => {
+ const testDir = 'test-entry-info'
+ const subDir = `${testDir}/subdir`
+
+ await sandbox.files.makeDir(testDir)
+ await sandbox.files.makeDir(subDir)
+
+ const files = await sandbox.files.list(testDir, { depth: 1 })
+ assert.equal(files.length, 1)
+
+ const dirEntry = files[0]
+ assert.equal(dirEntry.name, 'subdir')
+ assert.equal(dirEntry.path, `/home/user/${subDir}`)
+ assert.equal(dirEntry.type, 'dir')
+ assert.equal(dirEntry.mode, 0o755)
+ assert.equal(dirEntry.permissions, 'drwxr-xr-x')
+ assert.equal(dirEntry.owner, 'user')
+ assert.equal(dirEntry.group, 'user')
+ assert.ok(dirEntry.modifiedTime)
+})
+
+sandboxTest('mixed entries (files and directories)', async ({ sandbox }) => {
+ const testDir = 'test-mixed-entries'
+ const subDir = `${testDir}/subdir`
+ const filePath = `${testDir}/test.txt`
+ const content = 'Hello, World!'
+
+ await sandbox.files.makeDir(testDir)
+ await sandbox.files.makeDir(subDir)
+ await sandbox.files.write(filePath, content)
+
+ const files = await sandbox.files.list(testDir, { depth: 1 })
+ assert.equal(files.length, 2)
+
+ // Create a map of entries by name for easier verification
+ const entries = new Map(files.map((entry) => [entry.name, entry]))
+
+ // Verify directory entry
+ const dirEntry = entries.get('subdir')
+ assert.ok(dirEntry)
+ assert.equal(dirEntry!.path, `/home/user/${subDir}`)
+ assert.equal(dirEntry!.type, 'dir')
+ assert.equal(dirEntry!.mode, 0o755)
+ assert.equal(dirEntry!.permissions, 'drwxr-xr-x')
+ assert.equal(dirEntry!.owner, 'user')
+ assert.equal(dirEntry!.group, 'user')
+ assert.ok(dirEntry!.modifiedTime)
+
+ // Verify file entry
+ const fileEntry = entries.get('test.txt')
+ assert.ok(fileEntry)
+ assert.equal(fileEntry!.path, `/home/user/${filePath}`)
+ assert.equal(fileEntry!.type, 'file')
+ assert.equal(fileEntry!.mode, 0o644)
+ assert.equal(fileEntry!.permissions, '-rw-r--r--')
+ assert.equal(fileEntry!.owner, 'user')
+ assert.equal(fileEntry!.group, 'user')
+ assert.equal(fileEntry!.size, content.length)
+ assert.ok(fileEntry!.modifiedTime)
+})
diff --git a/packages/python-sdk/Makefile b/packages/python-sdk/Makefile
index fcec567913..d782751114 100644
--- a/packages/python-sdk/Makefile
+++ b/packages/python-sdk/Makefile
@@ -1,6 +1,8 @@
+ROOT_DIR := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))/../..)
+
generate-api:
- python ./../../spec/remove_extra_tags.py sandboxes
- openapi-python-client generate --output-path e2b/api/api --overwrite --path ../../spec/openapi_generated.yml
+ python $(ROOT_DIR)/spec/remove_extra_tags.py sandboxes
+ openapi-python-client generate --output-path $(ROOT_DIR)/packages/python-sdk/e2b/api/api --overwrite --path $(ROOT_DIR)/spec/openapi_generated.yml
rm -rf e2b/api/client
mv e2b/api/api/e2b_api_client e2b/api/client
rm -rf e2b/api/api
@@ -8,15 +10,15 @@ generate-api:
generate-envd:
if [ ! -f "/go/bin/protoc-gen-connect-python" ]; then \
- $(MAKE) -C packages/connect-python build; \
+ $(MAKE) -C $(ROOT_DIR)/packages/connect-python build; \
fi
- cd ./../../spec/envd && buf generate --template buf-python.gen.yaml
+ cd $(ROOT_DIR)/spec/envd && pwd && buf generate --template buf-python.gen.yaml
+ ./scripts/fix-python-pb.sh
+
black .
generate: generate-api generate-envd
- ./scripts/fix-python-pb.sh
- black .
init:
pip install openapi-python-client
diff --git a/packages/python-sdk/e2b/__init__.py b/packages/python-sdk/e2b/__init__.py
index c0c99d8754..9610017e1a 100644
--- a/packages/python-sdk/e2b/__init__.py
+++ b/packages/python-sdk/e2b/__init__.py
@@ -56,7 +56,7 @@
FilesystemEvent,
FilesystemEventType,
)
-from .sandbox.filesystem.filesystem import EntryInfo, FileType
+from .sandbox.filesystem.filesystem import EntryInfo, WriteInfo, FileType
from .sandbox_sync.main import Sandbox
from .sandbox_sync.filesystem.watch_handle import WatchHandle
@@ -96,6 +96,7 @@
"FilesystemEvent",
"FilesystemEventType",
"EntryInfo",
+ "WriteInfo",
"FileType",
# Sync sandbox
"Sandbox",
diff --git a/packages/python-sdk/e2b/envd/filesystem/filesystem_pb2.py b/packages/python-sdk/e2b/envd/filesystem/filesystem_pb2.py
index 1a0da40fa4..f7a1c3b8d1 100644
--- a/packages/python-sdk/e2b/envd/filesystem/filesystem_pb2.py
+++ b/packages/python-sdk/e2b/envd/filesystem/filesystem_pb2.py
@@ -18,8 +18,11 @@
_sym_db = _symbol_database.Default()
+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"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\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'
+ 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'
)
_globals = globals()
@@ -32,54 +35,54 @@
_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 = 1410
- _globals["_FILETYPE"]._serialized_end = 1492
- _globals["_EVENTTYPE"]._serialized_start = 1495
- _globals["_EVENTTYPE"]._serialized_end = 1647
- _globals["_MOVEREQUEST"]._serialized_start = 43
- _globals["_MOVEREQUEST"]._serialized_end = 114
- _globals["_MOVERESPONSE"]._serialized_start = 116
- _globals["_MOVERESPONSE"]._serialized_end = 175
- _globals["_MAKEDIRREQUEST"]._serialized_start = 177
- _globals["_MAKEDIRREQUEST"]._serialized_end = 213
- _globals["_MAKEDIRRESPONSE"]._serialized_start = 215
- _globals["_MAKEDIRRESPONSE"]._serialized_end = 277
- _globals["_REMOVEREQUEST"]._serialized_start = 279
- _globals["_REMOVEREQUEST"]._serialized_end = 314
- _globals["_REMOVERESPONSE"]._serialized_start = 316
- _globals["_REMOVERESPONSE"]._serialized_end = 332
- _globals["_STATREQUEST"]._serialized_start = 334
- _globals["_STATREQUEST"]._serialized_end = 367
- _globals["_STATRESPONSE"]._serialized_start = 369
- _globals["_STATRESPONSE"]._serialized_end = 428
- _globals["_ENTRYINFO"]._serialized_start = 430
- _globals["_ENTRYINFO"]._serialized_end = 523
- _globals["_LISTDIRREQUEST"]._serialized_start = 525
- _globals["_LISTDIRREQUEST"]._serialized_end = 583
- _globals["_LISTDIRRESPONSE"]._serialized_start = 585
- _globals["_LISTDIRRESPONSE"]._serialized_end = 651
- _globals["_WATCHDIRREQUEST"]._serialized_start = 653
- _globals["_WATCHDIRREQUEST"]._serialized_end = 720
- _globals["_FILESYSTEMEVENT"]._serialized_start = 722
- _globals["_FILESYSTEMEVENT"]._serialized_end = 802
- _globals["_WATCHDIRRESPONSE"]._serialized_start = 805
- _globals["_WATCHDIRRESPONSE"]._serialized_end = 1059
- _globals["_WATCHDIRRESPONSE_STARTEVENT"]._serialized_start = 1025
- _globals["_WATCHDIRRESPONSE_STARTEVENT"]._serialized_end = 1037
- _globals["_WATCHDIRRESPONSE_KEEPALIVE"]._serialized_start = 1039
- _globals["_WATCHDIRRESPONSE_KEEPALIVE"]._serialized_end = 1050
- _globals["_CREATEWATCHERREQUEST"]._serialized_start = 1061
- _globals["_CREATEWATCHERREQUEST"]._serialized_end = 1133
- _globals["_CREATEWATCHERRESPONSE"]._serialized_start = 1135
- _globals["_CREATEWATCHERRESPONSE"]._serialized_end = 1189
- _globals["_GETWATCHEREVENTSREQUEST"]._serialized_start = 1191
- _globals["_GETWATCHEREVENTSREQUEST"]._serialized_end = 1247
- _globals["_GETWATCHEREVENTSRESPONSE"]._serialized_start = 1249
- _globals["_GETWATCHEREVENTSRESPONSE"]._serialized_end = 1328
- _globals["_REMOVEWATCHERREQUEST"]._serialized_start = 1330
- _globals["_REMOVEWATCHERREQUEST"]._serialized_end = 1383
- _globals["_REMOVEWATCHERRESPONSE"]._serialized_start = 1385
- _globals["_REMOVEWATCHERRESPONSE"]._serialized_end = 1408
- _globals["_FILESYSTEM"]._serialized_start = 1650
- _globals["_FILESYSTEM"]._serialized_end = 2321
+ _globals["_FILETYPE"]._serialized_start = 1690
+ _globals["_FILETYPE"]._serialized_end = 1772
+ _globals["_EVENTTYPE"]._serialized_start = 1775
+ _globals["_EVENTTYPE"]._serialized_end = 1927
+ _globals["_MOVEREQUEST"]._serialized_start = 76
+ _globals["_MOVEREQUEST"]._serialized_end = 147
+ _globals["_MOVERESPONSE"]._serialized_start = 149
+ _globals["_MOVERESPONSE"]._serialized_end = 208
+ _globals["_MAKEDIRREQUEST"]._serialized_start = 210
+ _globals["_MAKEDIRREQUEST"]._serialized_end = 246
+ _globals["_MAKEDIRRESPONSE"]._serialized_start = 248
+ _globals["_MAKEDIRRESPONSE"]._serialized_end = 310
+ _globals["_REMOVEREQUEST"]._serialized_start = 312
+ _globals["_REMOVEREQUEST"]._serialized_end = 347
+ _globals["_REMOVERESPONSE"]._serialized_start = 349
+ _globals["_REMOVERESPONSE"]._serialized_end = 365
+ _globals["_STATREQUEST"]._serialized_start = 367
+ _globals["_STATREQUEST"]._serialized_end = 400
+ _globals["_STATRESPONSE"]._serialized_start = 402
+ _globals["_STATRESPONSE"]._serialized_end = 461
+ _globals["_ENTRYINFO"]._serialized_start = 464
+ _globals["_ENTRYINFO"]._serialized_end = 803
+ _globals["_LISTDIRREQUEST"]._serialized_start = 805
+ _globals["_LISTDIRREQUEST"]._serialized_end = 863
+ _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
# @@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 0b93efc1d6..e1843c3f93 100644
--- a/packages/python-sdk/e2b/envd/filesystem/filesystem_pb2.pyi
+++ b/packages/python-sdk/e2b/envd/filesystem/filesystem_pb2.pyi
@@ -1,3 +1,4 @@
+from google.protobuf import timestamp_pb2 as _timestamp_pb2
from google.protobuf.internal import containers as _containers
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
from google.protobuf import descriptor as _descriptor
@@ -88,18 +89,50 @@ class StatResponse(_message.Message):
def __init__(self, entry: _Optional[_Union[EntryInfo, _Mapping]] = ...) -> None: ...
class EntryInfo(_message.Message):
- __slots__ = ("name", "type", "path")
+ __slots__ = (
+ "name",
+ "type",
+ "path",
+ "size",
+ "mode",
+ "permissions",
+ "owner",
+ "group",
+ "modified_time",
+ "symlink_target",
+ )
NAME_FIELD_NUMBER: _ClassVar[int]
TYPE_FIELD_NUMBER: _ClassVar[int]
PATH_FIELD_NUMBER: _ClassVar[int]
+ SIZE_FIELD_NUMBER: _ClassVar[int]
+ MODE_FIELD_NUMBER: _ClassVar[int]
+ PERMISSIONS_FIELD_NUMBER: _ClassVar[int]
+ OWNER_FIELD_NUMBER: _ClassVar[int]
+ GROUP_FIELD_NUMBER: _ClassVar[int]
+ MODIFIED_TIME_FIELD_NUMBER: _ClassVar[int]
+ SYMLINK_TARGET_FIELD_NUMBER: _ClassVar[int]
name: str
type: FileType
path: str
+ size: int
+ mode: int
+ permissions: str
+ owner: str
+ group: str
+ modified_time: _timestamp_pb2.Timestamp
+ symlink_target: str
def __init__(
self,
name: _Optional[str] = ...,
type: _Optional[_Union[FileType, str]] = ...,
path: _Optional[str] = ...,
+ size: _Optional[int] = ...,
+ mode: _Optional[int] = ...,
+ permissions: _Optional[str] = ...,
+ owner: _Optional[str] = ...,
+ group: _Optional[str] = ...,
+ modified_time: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ...,
+ symlink_target: _Optional[str] = ...,
) -> None: ...
class ListDirRequest(_message.Message):
diff --git a/packages/python-sdk/e2b/sandbox/filesystem/filesystem.py b/packages/python-sdk/e2b/sandbox/filesystem/filesystem.py
index 9874f3e77c..a144c8e2ab 100644
--- a/packages/python-sdk/e2b/sandbox/filesystem/filesystem.py
+++ b/packages/python-sdk/e2b/sandbox/filesystem/filesystem.py
@@ -1,4 +1,5 @@
from dataclasses import dataclass
+from datetime import datetime
from enum import Enum
from typing import IO, Optional, Union
@@ -28,7 +29,7 @@ def map_file_type(ft: filesystem_pb2.FileType):
@dataclass
-class EntryInfo:
+class WriteInfo:
"""
Sandbox filesystem object information.
"""
@@ -47,6 +48,43 @@ class EntryInfo:
"""
+@dataclass
+class EntryInfo(WriteInfo):
+ """
+ Extended sandbox filesystem object information.
+ """
+
+ size: int
+ """
+ Size of the filesystem object in bytes.
+ """
+ mode: int
+ """
+ File mode and permission bits.
+ """
+ permissions: str
+ """
+ String representation of file permissions (e.g. 'rwxr-xr-x').
+ """
+ owner: str
+ """
+ Owner of the filesystem object.
+ """
+ group: str
+ """
+ Group owner of the filesystem object.
+ """
+ modified_time: datetime
+ """
+ Last modification time of the filesystem object.
+ """
+ symlink_target: Optional[str] = None
+ """
+ Target of the symlink if the filesystem object is a symlink.
+ If the filesystem object is not a symlink, this field is None.
+ """
+
+
@dataclass
class WriteEntry:
"""
diff --git a/packages/python-sdk/e2b/sandbox_async/filesystem/filesystem.py b/packages/python-sdk/e2b/sandbox_async/filesystem/filesystem.py
index f28dfd0951..78e87accaf 100644
--- a/packages/python-sdk/e2b/sandbox_async/filesystem/filesystem.py
+++ b/packages/python-sdk/e2b/sandbox_async/filesystem/filesystem.py
@@ -16,7 +16,11 @@
from e2b.envd.rpc import authentication_header, handle_rpc_exception
from e2b.envd.versions import ENVD_VERSION_RECURSIVE_WATCH
from e2b.exceptions import SandboxException, TemplateException, InvalidArgumentException
-from e2b.sandbox.filesystem.filesystem import EntryInfo, map_file_type
+from e2b.sandbox.filesystem.filesystem import (
+ WriteInfo,
+ EntryInfo,
+ map_file_type,
+)
from e2b.sandbox.filesystem.watch_handle import FilesystemEvent
from e2b.sandbox_async.filesystem.watch_handle import AsyncWatchHandle
from e2b.sandbox_async.utils import OutputHandler
@@ -141,7 +145,7 @@ async def write(
data: Union[str, bytes, IO],
user: Username = "user",
request_timeout: Optional[float] = None,
- ) -> EntryInfo:
+ ) -> WriteInfo:
"""
Write content to a file on the path.
@@ -165,7 +169,7 @@ async def write(
files: List[WriteEntry],
user: Optional[Username] = "user",
request_timeout: Optional[float] = None,
- ) -> List[EntryInfo]:
+ ) -> List[WriteInfo]:
"""
Writes multiple files.
@@ -181,7 +185,7 @@ async def write(
data_or_user: Union[str, bytes, IO, Username] = "user",
user_or_request_timeout: Optional[Union[float, Username]] = None,
request_timeout_or_none: Optional[float] = None,
- ) -> Union[EntryInfo, List[EntryInfo]]:
+ ) -> Union[WriteInfo, List[WriteInfo]]:
"""
Writes content to a file on the path.
When writing to a file that doesn't exist, the file will get created.
@@ -247,9 +251,9 @@ async def write(
if len(write_files) == 1 and path:
file = write_files[0]
- return EntryInfo(**file)
+ return WriteInfo(**file)
else:
- return [EntryInfo(**file) for file in write_files]
+ return [WriteInfo(**file) for file in write_files]
async def list(
self,
@@ -286,7 +290,23 @@ async def list(
if event_type:
entries.append(
- EntryInfo(name=entry.name, type=event_type, path=entry.path)
+ 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
+ ),
+ )
)
return entries
@@ -325,6 +345,49 @@ async def exists(
return False
raise handle_rpc_exception(e)
+ async def get_info(
+ self,
+ path: str,
+ user: Username = "user",
+ request_timeout: Optional[float] = None,
+ ) -> EntryInfo:
+ """
+ Get information about a file or directory.
+
+ :param path: Path to a file or a directory
+ :param user: Run the operation as this user
+ :param request_timeout: Timeout for the request in **seconds**
+
+ :return: Information about the file or directory like name, type, and path
+ """
+ try:
+ r = await self._rpc.astat(
+ filesystem_pb2.StatRequest(path=path),
+ request_timeout=self._connection_config.get_request_timeout(
+ request_timeout
+ ),
+ headers=authentication_header(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
+ ),
+ )
+ except Exception as e:
+ raise handle_rpc_exception(e)
+
async def remove(
self,
path: str,
@@ -382,6 +445,18 @@ async def rename(
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
+ ),
)
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 a7ae9320cb..56ac2421d2 100644
--- a/packages/python-sdk/e2b/sandbox_sync/filesystem/filesystem.py
+++ b/packages/python-sdk/e2b/sandbox_sync/filesystem/filesystem.py
@@ -18,7 +18,11 @@
from e2b.envd.api import ENVD_API_FILES_ROUTE, handle_envd_api_exception
from e2b.envd.filesystem import filesystem_connect, filesystem_pb2
from e2b.envd.rpc import authentication_header, handle_rpc_exception
-from e2b.sandbox.filesystem.filesystem import EntryInfo, map_file_type
+from e2b.sandbox.filesystem.filesystem import (
+ WriteInfo,
+ EntryInfo,
+ map_file_type,
+)
from e2b.sandbox_sync.filesystem.watch_handle import WatchHandle
@@ -141,7 +145,7 @@ def write(
data: Union[str, bytes, IO],
user: Username = "user",
request_timeout: Optional[float] = None,
- ) -> EntryInfo:
+ ) -> WriteInfo:
"""
Write content to a file on the path.
@@ -165,7 +169,7 @@ def write(
files: List[WriteEntry],
user: Optional[Username] = "user",
request_timeout: Optional[float] = None,
- ) -> List[EntryInfo]:
+ ) -> List[WriteInfo]:
"""
Writes a list of files to the filesystem.
When writing to a file that doesn't exist, the file will get created.
@@ -184,7 +188,7 @@ def write(
data_or_user: Union[str, bytes, IO, Username] = "user",
user_or_request_timeout: Optional[Union[float, Username]] = None,
request_timeout_or_none: Optional[float] = None,
- ) -> Union[EntryInfo, List[EntryInfo]]:
+ ) -> Union[WriteInfo, List[WriteInfo]]:
path, write_files, user, request_timeout = None, [], "user", None
if isinstance(path_or_files, str):
if isinstance(data_or_user, list):
@@ -244,9 +248,9 @@ def write(
if len(write_files) == 1 and path:
file = write_files[0]
- return EntryInfo(**file)
+ return WriteInfo(**file)
else:
- return [EntryInfo(**file) for file in write_files]
+ return [WriteInfo(**file) for file in write_files]
def list(
self,
@@ -283,7 +287,23 @@ def list(
if event_type:
entries.append(
- EntryInfo(name=entry.name, type=event_type, path=entry.path)
+ 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
+ ),
+ )
)
return entries
@@ -321,6 +341,50 @@ def exists(
return False
raise handle_rpc_exception(e)
+ def get_info(
+ self,
+ path: str,
+ user: Username = "user",
+ request_timeout: Optional[float] = None,
+ ) -> EntryInfo:
+ """
+ Get information about a file or directory.
+
+ :param path: Path to a file or a directory
+ :param user: Run the operation as this user
+ :param request_timeout: Timeout for the request in **seconds**
+
+ :return: Information about the file or directory like name, type, and path
+ """
+ try:
+ r = self._rpc.stat(
+ filesystem_pb2.StatRequest(path=path),
+ request_timeout=self._connection_config.get_request_timeout(
+ request_timeout
+ ),
+ headers=authentication_header(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
+ ),
+ )
+ except Exception as e:
+ raise handle_rpc_exception(e)
+
def remove(
self,
path: str,
@@ -378,6 +442,18 @@ def rename(
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
+ ),
)
except Exception as e:
raise handle_rpc_exception(e)
diff --git a/packages/python-sdk/pyproject.toml b/packages/python-sdk/pyproject.toml
index 8f783ec689..aafa615e43 100644
--- a/packages/python-sdk/pyproject.toml
+++ b/packages/python-sdk/pyproject.toml
@@ -36,3 +36,8 @@ build-backend = "poetry.core.masonry.api"
[tool.poetry.urls]
"Bug Tracker" = "https://github.com/e2b-dev/e2b/issues"
+
+[tool.ruff]
+exclude = [
+ "e2b/envd/filesystem/filesystem_pb2.py"
+]
\ No newline at end of file
diff --git a/packages/python-sdk/tests/async/sandbox_async/files/test_files_list.py b/packages/python-sdk/tests/async/sandbox_async/files/test_files_list.py
index 39104a1aa1..23034f75a0 100644
--- a/packages/python-sdk/tests/async/sandbox_async/files/test_files_list.py
+++ b/packages/python-sdk/tests/async/sandbox_async/files/test_files_list.py
@@ -153,3 +153,95 @@ async def test_list_directory_error_cases(async_sandbox: AsyncSandbox):
), f'expected error message to include "{expected_error_message}"'
await async_sandbox.files.remove(parent_dir_name)
+
+
+async def test_file_entry_details(async_sandbox: AsyncSandbox):
+ test_dir = "test-file-entry"
+ file_path = f"{test_dir}/test.txt"
+ content = "Hello, World!"
+
+ await async_sandbox.files.make_dir(test_dir)
+ await async_sandbox.files.write(file_path, content)
+
+ files = await async_sandbox.files.list(test_dir, depth=1)
+ assert len(files) == 1
+
+ file_entry = files[0]
+ assert file_entry.name == "test.txt"
+ assert file_entry.path == f"/home/user/{file_path}"
+ assert file_entry.type == FileType.FILE
+ assert file_entry.mode == 0o644
+ assert file_entry.permissions == "-rw-r--r--"
+ assert file_entry.owner == "user"
+ assert file_entry.group == "user"
+ assert file_entry.size == len(content)
+ assert file_entry.modified_time is not None
+ assert file_entry.symlink_target is None
+
+ await async_sandbox.files.remove(test_dir)
+
+
+async def test_directory_entry_details(async_sandbox: AsyncSandbox):
+ test_dir = "test-entry-info"
+ sub_dir = f"{test_dir}/subdir"
+
+ await async_sandbox.files.make_dir(test_dir)
+ await async_sandbox.files.make_dir(sub_dir)
+
+ files = await async_sandbox.files.list(test_dir, depth=1)
+ assert len(files) == 1
+
+ dir_entry = files[0]
+ assert dir_entry.name == "subdir"
+ assert dir_entry.path == f"/home/user/{sub_dir}"
+ assert dir_entry.type == FileType.DIR
+ assert dir_entry.mode == 0o755
+ assert dir_entry.permissions == "drwxr-xr-x"
+ assert dir_entry.owner == "user"
+ assert dir_entry.group == "user"
+ assert dir_entry.modified_time is not None
+ assert dir_entry.symlink_target is None
+
+ await async_sandbox.files.remove(test_dir)
+
+
+async def test_mixed_entries(async_sandbox: AsyncSandbox):
+ test_dir = "test-mixed-entries"
+ sub_dir = f"{test_dir}/subdir"
+ file_path = f"{test_dir}/test.txt"
+ content = "Hello, World!"
+
+ await async_sandbox.files.make_dir(test_dir)
+ await async_sandbox.files.make_dir(sub_dir)
+ await async_sandbox.files.write(file_path, content)
+
+ files = await async_sandbox.files.list(test_dir, depth=1)
+ assert len(files) == 2
+
+ # Create a dictionary of entries by name for easier verification
+ entries = {entry.name: entry for entry in files}
+
+ # Verify directory entry
+ dir_entry = entries.get("subdir")
+ assert dir_entry is not None
+ assert dir_entry.path == f"/home/user/{sub_dir}"
+ assert dir_entry.type == FileType.DIR
+ assert dir_entry.mode == 0o755
+ assert dir_entry.permissions == "drwxr-xr-x"
+ assert dir_entry.owner == "user"
+ assert dir_entry.group == "user"
+ assert dir_entry.modified_time is not None
+
+ # Verify file entry
+ file_entry = entries.get("test.txt")
+ assert file_entry is not None
+ assert file_entry.path == f"/home/user/{file_path}"
+ assert file_entry.type == FileType.FILE
+ assert file_entry.mode == 0o644
+ assert file_entry.permissions == "-rw-r--r--"
+ assert file_entry.owner == "user"
+ assert file_entry.group == "user"
+ assert file_entry.size == len(content)
+ assert file_entry.modified_time is not None
+
+ await async_sandbox.files.remove(test_dir)
diff --git a/packages/python-sdk/tests/async/sandbox_async/files/test_info.py b/packages/python-sdk/tests/async/sandbox_async/files/test_info.py
new file mode 100644
index 0000000000..383643d77d
--- /dev/null
+++ b/packages/python-sdk/tests/async/sandbox_async/files/test_info.py
@@ -0,0 +1,77 @@
+import pytest
+from e2b.exceptions import NotFoundException
+from e2b import AsyncSandbox, FileType
+
+
+@pytest.mark.asyncio
+async def test_get_info_of_file(async_sandbox: AsyncSandbox):
+ filename = "test_file.txt"
+
+ await async_sandbox.files.write(filename, "test")
+ info = await async_sandbox.files.get_info(filename)
+ current_path = await async_sandbox.commands.run("pwd")
+
+ assert info.name == filename
+ assert info.type == FileType.FILE
+ assert info.path == f"{current_path.stdout.strip()}/{filename}"
+ assert info.size == 4
+ assert info.mode == 0o644
+ assert info.permissions == "-rw-r--r--"
+ assert info.owner == "user"
+ assert info.group == "user"
+ assert info.modified_time is not None
+
+
+@pytest.mark.asyncio
+async def test_get_info_of_nonexistent_file(async_sandbox: AsyncSandbox):
+ filename = "test_does_not_exist.txt"
+
+ with pytest.raises(NotFoundException):
+ await async_sandbox.files.get_info(filename)
+
+
+@pytest.mark.asyncio
+async def test_get_info_of_directory(async_sandbox: AsyncSandbox):
+ dirname = "test_dir"
+
+ await async_sandbox.files.make_dir(dirname)
+ info = await async_sandbox.files.get_info(dirname)
+ current_path = await async_sandbox.commands.run("pwd")
+
+ assert info.name == dirname
+ assert info.type == FileType.DIR
+ assert info.path == f"{current_path.stdout.strip()}/{dirname}"
+ assert info.size > 0
+ assert info.mode == 0o755
+ assert info.permissions == "drwxr-xr-x"
+ assert info.owner == "user"
+ assert info.group == "user"
+ assert info.modified_time is not None
+
+
+@pytest.mark.asyncio
+async def test_get_info_of_nonexistent_directory(async_sandbox: AsyncSandbox):
+ dirname = "test_does_not_exist_dir"
+
+ with pytest.raises(NotFoundException):
+ await async_sandbox.files.get_info(dirname)
+
+
+async def test_file_symlink(async_sandbox: AsyncSandbox):
+ test_dir = "test-simlink-entry"
+ file_name = "test.txt"
+ content = "Hello, World!"
+
+ await async_sandbox.files.make_dir(test_dir)
+ await async_sandbox.files.write(f"{test_dir}/{file_name}", content)
+
+ symlink_name = "symlink_to_test.txt"
+ await async_sandbox.commands.run(f"ln -s {file_name} {symlink_name}", cwd=test_dir)
+
+ file = await async_sandbox.files.get_info(f"{test_dir}/{symlink_name}")
+
+ pwd = await async_sandbox.commands.run("pwd")
+ assert file.type == FileType.FILE
+ assert file.symlink_target == f"{pwd.stdout.strip()}/{test_dir}/{file_name}"
+
+ await async_sandbox.files.remove(test_dir)
diff --git a/packages/python-sdk/tests/async/sandbox_async/files/test_write.py b/packages/python-sdk/tests/async/sandbox_async/files/test_write.py
index 46a065c951..bf7b57da9e 100644
--- a/packages/python-sdk/tests/async/sandbox_async/files/test_write.py
+++ b/packages/python-sdk/tests/async/sandbox_async/files/test_write.py
@@ -2,7 +2,7 @@
import uuid
from e2b import AsyncSandbox
-from e2b.sandbox_async.filesystem.filesystem import EntryInfo
+from e2b.sandbox_async.filesystem.filesystem import WriteInfo
async def test_write_text_file(async_sandbox: AsyncSandbox):
@@ -26,7 +26,7 @@ async def test_write_text_file(async_sandbox: AsyncSandbox):
async def test_write_binary_file(async_sandbox: AsyncSandbox):
- filename = "test_write.txt"
+ filename = "test_write.bin"
text = "This is a test binary file."
# equivalent to `open("path/to/local/file", "rb")`
content = io.BytesIO(text.encode("utf-8"))
@@ -72,7 +72,7 @@ async def test_write_multiple_files(async_sandbox: AsyncSandbox):
assert isinstance(info, list)
assert len(info) == 1
info = info[0]
- assert isinstance(info, EntryInfo)
+ assert isinstance(info, WriteInfo)
assert info.path == "/home/user/one_test_file.txt"
exists = await async_sandbox.files.exists(info.path)
assert exists
@@ -91,7 +91,7 @@ async def test_write_multiple_files(async_sandbox: AsyncSandbox):
assert isinstance(infos, list)
assert len(infos) == len(files)
for i, info in enumerate(infos):
- assert isinstance(info, EntryInfo)
+ assert isinstance(info, WriteInfo)
assert info.path == f"/home/user/test_write_{i}.txt"
exists = await async_sandbox.files.exists(path)
assert exists
diff --git a/packages/python-sdk/tests/sync/sandbox_sync/files/test_files_list.py b/packages/python-sdk/tests/sync/sandbox_sync/files/test_files_list.py
index 9e265ef34a..59782a67a1 100644
--- a/packages/python-sdk/tests/sync/sandbox_sync/files/test_files_list.py
+++ b/packages/python-sdk/tests/sync/sandbox_sync/files/test_files_list.py
@@ -153,3 +153,94 @@ def test_list_directory_error_cases(sandbox: Sandbox):
), f'expected error message to include "{expected_error_message}"'
sandbox.files.remove(parent_dir_name)
+
+
+def test_file_entry_details(sandbox: Sandbox):
+ test_dir = "test-file-entry"
+ file_path = f"{test_dir}/test.txt"
+ content = "Hello, World!"
+
+ sandbox.files.make_dir(test_dir)
+ sandbox.files.write(file_path, content)
+
+ files = sandbox.files.list(test_dir, depth=1)
+ assert len(files) == 1
+
+ file_entry = files[0]
+ assert file_entry.name == "test.txt"
+ assert file_entry.path == f"/home/user/{file_path}"
+ assert file_entry.type == FileType.FILE
+ assert file_entry.mode == 0o644
+ assert file_entry.permissions == "-rw-r--r--"
+ assert file_entry.owner == "user"
+ assert file_entry.group == "user"
+ assert file_entry.size == len(content)
+ assert file_entry.modified_time is not None
+ assert file_entry.symlink_target is None
+
+ sandbox.files.remove(test_dir)
+
+
+def test_directory_entry_details(sandbox: Sandbox):
+ test_dir = "test-entry-info"
+ sub_dir = f"{test_dir}/subdir"
+
+ sandbox.files.make_dir(test_dir)
+ sandbox.files.make_dir(sub_dir)
+
+ files = sandbox.files.list(test_dir, depth=1)
+ assert len(files) == 1
+
+ dir_entry = files[0]
+ assert dir_entry.name == "subdir"
+ assert dir_entry.path == f"/home/user/{sub_dir}"
+ assert dir_entry.type == FileType.DIR
+ assert dir_entry.mode == 0o755
+ assert dir_entry.permissions == "drwxr-xr-x"
+ assert dir_entry.owner == "user"
+ assert dir_entry.group == "user"
+ assert dir_entry.modified_time is not None
+
+ sandbox.files.remove(test_dir)
+
+
+def test_mixed_entries(sandbox: Sandbox):
+ test_dir = "test-mixed-entries"
+ sub_dir = f"{test_dir}/subdir"
+ file_path = f"{test_dir}/test.txt"
+ content = "Hello, World!"
+
+ sandbox.files.make_dir(test_dir)
+ sandbox.files.make_dir(sub_dir)
+ sandbox.files.write(file_path, content)
+
+ files = sandbox.files.list(test_dir, depth=1)
+ assert len(files) == 2
+
+ # Create a dictionary of entries by name for easier verification
+ entries = {entry.name: entry for entry in files}
+
+ # Verify directory entry
+ dir_entry = entries.get("subdir")
+ assert dir_entry is not None
+ assert dir_entry.path == f"/home/user/{sub_dir}"
+ assert dir_entry.type == FileType.DIR
+ assert dir_entry.mode == 0o755
+ assert dir_entry.permissions == "drwxr-xr-x"
+ assert dir_entry.owner == "user"
+ assert dir_entry.group == "user"
+ assert dir_entry.modified_time is not None
+
+ # Verify file entry
+ file_entry = entries.get("test.txt")
+ assert file_entry is not None
+ assert file_entry.path == f"/home/user/{file_path}"
+ assert file_entry.type == FileType.FILE
+ assert file_entry.mode == 0o644
+ assert file_entry.permissions == "-rw-r--r--"
+ assert file_entry.owner == "user"
+ assert file_entry.group == "user"
+ assert file_entry.size == len(content)
+ assert file_entry.modified_time is not None
+
+ sandbox.files.remove(test_dir)
diff --git a/packages/python-sdk/tests/sync/sandbox_sync/files/test_info.py b/packages/python-sdk/tests/sync/sandbox_sync/files/test_info.py
new file mode 100644
index 0000000000..35e18ac840
--- /dev/null
+++ b/packages/python-sdk/tests/sync/sandbox_sync/files/test_info.py
@@ -0,0 +1,73 @@
+import pytest
+from e2b.exceptions import NotFoundException
+from e2b import Sandbox, FileType
+
+
+def test_get_info_of_file(sandbox: Sandbox):
+ filename = "test_file.txt"
+
+ sandbox.files.write(filename, "test")
+ info = sandbox.files.get_info(filename)
+ current_path = sandbox.commands.run("pwd")
+
+ assert info.name == filename
+ assert info.type == FileType.FILE
+ assert info.path == f"{current_path.stdout.strip()}/{filename}"
+ assert info.size == 4
+ assert info.mode == 0o644
+ assert info.permissions == "-rw-r--r--"
+ assert info.owner == "user"
+ assert info.group == "user"
+ assert info.modified_time is not None
+
+
+def test_get_info_of_nonexistent_file(sandbox: Sandbox):
+ filename = "test_does_not_exist.txt"
+
+ with pytest.raises(NotFoundException):
+ sandbox.files.get_info(filename)
+
+
+def test_get_info_of_directory(sandbox: Sandbox):
+ dirname = "test_dir"
+
+ sandbox.files.make_dir(dirname)
+ info = sandbox.files.get_info(dirname)
+ current_path = sandbox.commands.run("pwd")
+
+ assert info.name == dirname
+ assert info.type == FileType.DIR
+ assert info.path == f"{current_path.stdout.strip()}/{dirname}"
+ assert info.size > 0
+ assert info.mode == 0o755
+ assert info.permissions == "drwxr-xr-x"
+ assert info.owner == "user"
+ assert info.group == "user"
+ assert info.modified_time is not None
+
+
+def test_get_info_of_nonexistent_directory(sandbox: Sandbox):
+ dirname = "test_does_not_exist_dir"
+
+ with pytest.raises(NotFoundException):
+ sandbox.files.get_info(dirname)
+
+
+def test_file_symlink(sandbox: Sandbox):
+ test_dir = "test-simlink-entry"
+ file_name = "test.txt"
+ content = "Hello, World!"
+
+ sandbox.files.make_dir(test_dir)
+ sandbox.files.write(f"{test_dir}/{file_name}", content)
+
+ symlink_name = "symlink_to_test.txt"
+ sandbox.commands.run(f"ln -s {file_name} {symlink_name}", cwd=test_dir)
+
+ file = sandbox.files.get_info(f"{test_dir}/{symlink_name}")
+
+ pwd = sandbox.commands.run("pwd")
+ assert file.type == FileType.FILE
+ assert file.symlink_target == f"{pwd.stdout.strip()}/{test_dir}/{file_name}"
+
+ sandbox.files.remove(test_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 e8f8bc4b66..567e270002 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
@@ -22,8 +22,8 @@ def test_watch_directory_changes(sandbox: Sandbox):
def test_watch_iterated(sandbox: Sandbox):
- dirname = "test_watch_dir"
- filename = "test_watch.txt"
+ dirname = "test_watch_dir_iterated"
+ filename = "test_watch_iterated.txt"
content = "This file will be watched."
new_content = "This file has been modified."
diff --git a/packages/python-sdk/tests/sync/sandbox_sync/files/test_write.py b/packages/python-sdk/tests/sync/sandbox_sync/files/test_write.py
index 8ea5a5e4fa..87d6f379f3 100644
--- a/packages/python-sdk/tests/sync/sandbox_sync/files/test_write.py
+++ b/packages/python-sdk/tests/sync/sandbox_sync/files/test_write.py
@@ -1,7 +1,7 @@
import io
import uuid
-from e2b.sandbox.filesystem.filesystem import EntryInfo
+from e2b.sandbox.filesystem.filesystem import WriteInfo
from e2b.sandbox_sync.main import Sandbox
@@ -26,7 +26,7 @@ def test_write_text_file(sandbox):
def test_write_binary_file(sandbox):
- filename = "test_write.txt"
+ filename = "test_write.bin"
text = "This is a test binary file."
# equivalent to `open("path/to/local/file", "rb")`
content = io.BytesIO(text.encode("utf-8"))
@@ -72,7 +72,7 @@ def test_write_multiple_files(sandbox):
assert isinstance(info, list)
assert len(info) == 1
info = info[0]
- assert isinstance(info, EntryInfo)
+ assert isinstance(info, WriteInfo)
assert info.path == "/home/user/one_test_file.txt"
exists = sandbox.files.exists(info.path)
assert exists
@@ -91,7 +91,7 @@ def test_write_multiple_files(sandbox):
assert isinstance(infos, list)
assert len(infos) == len(files)
for i, info in enumerate(infos):
- assert isinstance(info, EntryInfo)
+ assert isinstance(info, WriteInfo)
assert info.path == f"/home/user/test_write_{i}.txt"
exists = sandbox.files.exists(path)
assert exists
diff --git a/spec/envd/buf-python.gen.yaml b/spec/envd/buf-python.gen.yaml
index 286053db90..e84a2a852f 100644
--- a/spec/envd/buf-python.gen.yaml
+++ b/spec/envd/buf-python.gen.yaml
@@ -8,7 +8,7 @@ plugins:
- pyi_out=../../packages/python-sdk/e2b/envd
- name: connect-python
out: ../../packages/python-sdk/e2b/envd
- path: /go/bin/protoc-gen-connect-python
+ path: protoc-gen-connect-python
managed:
enabled: true
diff --git a/spec/envd/filesystem/filesystem.proto b/spec/envd/filesystem/filesystem.proto
index fc0976ac8a..ca9aeb12df 100644
--- a/spec/envd/filesystem/filesystem.proto
+++ b/spec/envd/filesystem/filesystem.proto
@@ -2,6 +2,8 @@ syntax = "proto3";
package filesystem;
+import "google/protobuf/timestamp.proto";
+
service Filesystem {
rpc Stat(StatRequest) returns (StatResponse);
rpc MakeDir(MakeDirRequest) returns (MakeDirResponse);
@@ -52,6 +54,14 @@ message EntryInfo {
string name = 1;
FileType type = 2;
string path = 3;
+ int64 size = 4;
+ uint32 mode = 5;
+ string permissions = 6;
+ string owner = 7;
+ string group = 8;
+ google.protobuf.Timestamp modified_time = 9;
+ // If the entry is a symlink, this field contains the target of the symlink.
+ optional string symlink_target = 10;
}
enum FileType {
@@ -109,7 +119,7 @@ message GetWatcherEventsResponse {
}
message RemoveWatcherRequest {
- string watcher_id = 1;
+ string watcher_id = 1;
}
message RemoveWatcherResponse {}
diff --git a/spec/envd/process/process.proto b/spec/envd/process/process.proto
index 0a2fad4b66..031e267347 100644
--- a/spec/envd/process/process.proto
+++ b/spec/envd/process/process.proto
@@ -28,7 +28,7 @@ message PTY {
message ProcessConfig {
string cmd = 1;
repeated string args = 2;
-
+
map envs = 3;
optional string cwd = 4;
}
@@ -45,7 +45,7 @@ message ListResponse {
repeated ProcessInfo processes = 1;
}
-message StartRequest {
+message StartRequest {
ProcessConfig process = 1;
optional PTY pty = 2;
optional string tag = 3;
@@ -66,11 +66,11 @@ message ProcessEvent {
EndEvent end = 3;
KeepAlive keepalive = 4;
}
-
+
message StartEvent {
uint32 pid = 1;
}
-
+
message DataEvent {
oneof output {
bytes stdout = 1;
@@ -78,7 +78,7 @@ message ProcessEvent {
bytes pty = 3;
}
}
-
+
message EndEvent {
sint32 exit_code = 1;
bool exited = 2;