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;