mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[codex] Add symlink flag to fs metadata (#17719)
Add `is_symlink` to FsMetadata struct.
This commit is contained in:
committed by
GitHub
Unverified
parent
495ed22dfb
commit
f3cbe3d385
@@ -7713,11 +7713,15 @@
|
||||
"type": "integer"
|
||||
},
|
||||
"isDirectory": {
|
||||
"description": "Whether the path currently resolves to a directory.",
|
||||
"description": "Whether the path resolves to a directory.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"isFile": {
|
||||
"description": "Whether the path currently resolves to a regular file.",
|
||||
"description": "Whether the path resolves to a regular file.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"isSymlink": {
|
||||
"description": "Whether the path itself is a symbolic link.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"modifiedAtMs": {
|
||||
@@ -7730,6 +7734,7 @@
|
||||
"createdAtMs",
|
||||
"isDirectory",
|
||||
"isFile",
|
||||
"isSymlink",
|
||||
"modifiedAtMs"
|
||||
],
|
||||
"title": "FsGetMetadataResponse",
|
||||
|
||||
@@ -4354,11 +4354,15 @@
|
||||
"type": "integer"
|
||||
},
|
||||
"isDirectory": {
|
||||
"description": "Whether the path currently resolves to a directory.",
|
||||
"description": "Whether the path resolves to a directory.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"isFile": {
|
||||
"description": "Whether the path currently resolves to a regular file.",
|
||||
"description": "Whether the path resolves to a regular file.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"isSymlink": {
|
||||
"description": "Whether the path itself is a symbolic link.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"modifiedAtMs": {
|
||||
@@ -4371,6 +4375,7 @@
|
||||
"createdAtMs",
|
||||
"isDirectory",
|
||||
"isFile",
|
||||
"isSymlink",
|
||||
"modifiedAtMs"
|
||||
],
|
||||
"title": "FsGetMetadataResponse",
|
||||
|
||||
@@ -8,11 +8,15 @@
|
||||
"type": "integer"
|
||||
},
|
||||
"isDirectory": {
|
||||
"description": "Whether the path currently resolves to a directory.",
|
||||
"description": "Whether the path resolves to a directory.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"isFile": {
|
||||
"description": "Whether the path currently resolves to a regular file.",
|
||||
"description": "Whether the path resolves to a regular file.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"isSymlink": {
|
||||
"description": "Whether the path itself is a symbolic link.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"modifiedAtMs": {
|
||||
@@ -25,6 +29,7 @@
|
||||
"createdAtMs",
|
||||
"isDirectory",
|
||||
"isFile",
|
||||
"isSymlink",
|
||||
"modifiedAtMs"
|
||||
],
|
||||
"title": "FsGetMetadataResponse",
|
||||
|
||||
@@ -7,13 +7,17 @@
|
||||
*/
|
||||
export type FsGetMetadataResponse = {
|
||||
/**
|
||||
* Whether the path currently resolves to a directory.
|
||||
* Whether the path resolves to a directory.
|
||||
*/
|
||||
isDirectory: boolean,
|
||||
/**
|
||||
* Whether the path currently resolves to a regular file.
|
||||
* Whether the path resolves to a regular file.
|
||||
*/
|
||||
isFile: boolean,
|
||||
/**
|
||||
* Whether the path itself is a symbolic link.
|
||||
*/
|
||||
isSymlink: boolean,
|
||||
/**
|
||||
* File creation time in Unix milliseconds when available, otherwise `0`.
|
||||
*/
|
||||
|
||||
@@ -2320,10 +2320,12 @@ pub struct FsGetMetadataParams {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct FsGetMetadataResponse {
|
||||
/// Whether the path currently resolves to a directory.
|
||||
/// Whether the path resolves to a directory.
|
||||
pub is_directory: bool,
|
||||
/// Whether the path currently resolves to a regular file.
|
||||
/// Whether the path resolves to a regular file.
|
||||
pub is_file: bool,
|
||||
/// Whether the path itself is a symbolic link.
|
||||
pub is_symlink: bool,
|
||||
/// File creation time in Unix milliseconds when available, otherwise `0`.
|
||||
#[ts(type = "number")]
|
||||
pub created_at_ms: i64,
|
||||
@@ -6765,6 +6767,7 @@ mod tests {
|
||||
let response = FsGetMetadataResponse {
|
||||
is_directory: false,
|
||||
is_file: true,
|
||||
is_symlink: false,
|
||||
created_at_ms: 123,
|
||||
modified_at_ms: 456,
|
||||
};
|
||||
@@ -6775,6 +6778,7 @@ mod tests {
|
||||
json!({
|
||||
"isDirectory": false,
|
||||
"isFile": true,
|
||||
"isSymlink": false,
|
||||
"createdAtMs": 123,
|
||||
"modifiedAtMs": 456,
|
||||
})
|
||||
|
||||
@@ -167,7 +167,7 @@ Example with notification opt-out:
|
||||
- `fs/readFile` — read an absolute file path and return `{ dataBase64 }`.
|
||||
- `fs/writeFile` — write an absolute file path from base64-encoded `{ dataBase64 }`; returns `{}`.
|
||||
- `fs/createDirectory` — create an absolute directory path; `recursive` defaults to `true`.
|
||||
- `fs/getMetadata` — return metadata for an absolute path: `isDirectory`, `isFile`, `createdAtMs`, and `modifiedAtMs`.
|
||||
- `fs/getMetadata` — return metadata for an absolute path: `isDirectory`, `isFile`, `isSymlink`, `createdAtMs`, and `modifiedAtMs`.
|
||||
- `fs/readDirectory` — list direct child entries for an absolute directory path; each entry contains `fileName`, `isDirectory`, and `isFile`, and `fileName` is just the child name, not a path.
|
||||
- `fs/remove` — remove an absolute file or directory tree; `recursive` and `force` default to `true`.
|
||||
- `fs/copy` — copy between absolute paths; directory copies require `recursive: true`.
|
||||
@@ -878,6 +878,7 @@ All filesystem paths in this section must be absolute.
|
||||
{ "id": 42, "result": {
|
||||
"isDirectory": false,
|
||||
"isFile": true,
|
||||
"isSymlink": false,
|
||||
"createdAtMs": 1730910000000,
|
||||
"modifiedAtMs": 1730910000000
|
||||
} }
|
||||
@@ -889,7 +890,7 @@ All filesystem paths in this section must be absolute.
|
||||
} }
|
||||
```
|
||||
|
||||
- `fs/getMetadata` returns whether the path currently resolves to a directory or regular file, plus `createdAtMs` and `modifiedAtMs` in Unix milliseconds. If a timestamp is unavailable on the current platform, that field is `0`.
|
||||
- `fs/getMetadata` returns whether the path resolves to a directory or regular file, whether the path itself is a symlink, plus `createdAtMs` and `modifiedAtMs` in Unix milliseconds. If a timestamp is unavailable on the current platform, that field is `0`.
|
||||
- `fs/createDirectory` defaults `recursive` to `true` when omitted.
|
||||
- `fs/remove` defaults both `recursive` and `force` to `true` when omitted.
|
||||
- `fs/readFile` always returns base64 bytes via `dataBase64`, and `fs/writeFile` always expects base64 bytes in `dataBase64`.
|
||||
|
||||
@@ -99,6 +99,7 @@ impl FsApi {
|
||||
Ok(FsGetMetadataResponse {
|
||||
is_directory: metadata.is_directory,
|
||||
is_file: metadata.is_file,
|
||||
is_symlink: metadata.is_symlink,
|
||||
created_at_ms: metadata.created_at_ms,
|
||||
modified_at_ms: metadata.modified_at_ms,
|
||||
})
|
||||
|
||||
@@ -89,6 +89,7 @@ async fn fs_get_metadata_returns_only_used_fields() -> Result<()> {
|
||||
"createdAtMs".to_string(),
|
||||
"isDirectory".to_string(),
|
||||
"isFile".to_string(),
|
||||
"isSymlink".to_string(),
|
||||
"modifiedAtMs".to_string(),
|
||||
]
|
||||
);
|
||||
@@ -99,6 +100,7 @@ async fn fs_get_metadata_returns_only_used_fields() -> Result<()> {
|
||||
FsGetMetadataResponse {
|
||||
is_directory: false,
|
||||
is_file: true,
|
||||
is_symlink: false,
|
||||
created_at_ms: stat.created_at_ms,
|
||||
modified_at_ms: stat.modified_at_ms,
|
||||
}
|
||||
@@ -111,6 +113,35 @@ async fn fs_get_metadata_returns_only_used_fields() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn fs_get_metadata_reports_symlink() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let file_path = codex_home.path().join("note.txt");
|
||||
let symlink_path = codex_home.path().join("note-link.txt");
|
||||
std::fs::write(&file_path, "hello")?;
|
||||
symlink(&file_path, &symlink_path)?;
|
||||
|
||||
let mut mcp = initialized_mcp(&codex_home).await?;
|
||||
let request_id = mcp
|
||||
.send_fs_get_metadata_request(codex_app_server_protocol::FsGetMetadataParams {
|
||||
path: absolute_path(symlink_path),
|
||||
})
|
||||
.await?;
|
||||
let response = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
|
||||
let stat: FsGetMetadataResponse = to_response(response)?;
|
||||
assert_eq!(stat.is_directory, false);
|
||||
assert_eq!(stat.is_file, true);
|
||||
assert_eq!(stat.is_symlink, true);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn fs_methods_cover_current_fs_utils_surface() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
@@ -25,6 +25,7 @@ pub struct CopyOptions {
|
||||
pub struct FileMetadata {
|
||||
pub is_directory: bool,
|
||||
pub is_file: bool,
|
||||
pub is_symlink: bool,
|
||||
pub created_at_ms: i64,
|
||||
pub modified_at_ms: i64,
|
||||
}
|
||||
|
||||
@@ -214,6 +214,7 @@ pub(crate) async fn run_direct_request(
|
||||
Ok(FsHelperPayload::GetMetadata(FsGetMetadataResponse {
|
||||
is_directory: metadata.is_directory,
|
||||
is_file: metadata.is_file,
|
||||
is_symlink: metadata.is_symlink,
|
||||
created_at_ms: metadata.created_at_ms,
|
||||
modified_at_ms: metadata.modified_at_ms,
|
||||
}))
|
||||
|
||||
@@ -286,9 +286,11 @@ impl ExecutorFileSystem for DirectFileSystem {
|
||||
) -> FileSystemResult<FileMetadata> {
|
||||
reject_sandbox_context(sandbox)?;
|
||||
let metadata = tokio::fs::metadata(path.as_path()).await?;
|
||||
let symlink_metadata = tokio::fs::symlink_metadata(path.as_path()).await?;
|
||||
Ok(FileMetadata {
|
||||
is_directory: metadata.is_dir(),
|
||||
is_file: metadata.is_file(),
|
||||
is_symlink: symlink_metadata.file_type().is_symlink(),
|
||||
created_at_ms: metadata.created().ok().map_or(0, system_time_to_unix_ms),
|
||||
modified_at_ms: metadata.modified().ok().map_or(0, system_time_to_unix_ms),
|
||||
})
|
||||
|
||||
@@ -199,6 +199,7 @@ pub struct FsGetMetadataParams {
|
||||
pub struct FsGetMetadataResponse {
|
||||
pub is_directory: bool,
|
||||
pub is_file: bool,
|
||||
pub is_symlink: bool,
|
||||
pub created_at_ms: i64,
|
||||
pub modified_at_ms: i64,
|
||||
}
|
||||
|
||||
@@ -115,6 +115,7 @@ impl ExecutorFileSystem for RemoteFileSystem {
|
||||
Ok(FileMetadata {
|
||||
is_directory: response.is_directory,
|
||||
is_file: response.is_file,
|
||||
is_symlink: response.is_symlink,
|
||||
created_at_ms: response.created_at_ms,
|
||||
modified_at_ms: response.modified_at_ms,
|
||||
})
|
||||
|
||||
@@ -138,6 +138,7 @@ impl ExecutorFileSystem for SandboxedFileSystem {
|
||||
Ok(FileMetadata {
|
||||
is_directory: response.is_directory,
|
||||
is_file: response.is_file,
|
||||
is_symlink: response.is_symlink,
|
||||
created_at_ms: response.created_at_ms,
|
||||
modified_at_ms: response.modified_at_ms,
|
||||
})
|
||||
|
||||
@@ -100,6 +100,7 @@ impl FileSystemHandler {
|
||||
Ok(FsGetMetadataResponse {
|
||||
is_directory: metadata.is_directory,
|
||||
is_file: metadata.is_file,
|
||||
is_symlink: metadata.is_symlink,
|
||||
created_at_ms: metadata.created_at_ms,
|
||||
modified_at_ms: metadata.modified_at_ms,
|
||||
})
|
||||
|
||||
@@ -141,13 +141,37 @@ async fn file_system_get_metadata_returns_expected_fields(use_remote: bool) -> R
|
||||
std::fs::write(&file_path, "hello")?;
|
||||
|
||||
let metadata = file_system
|
||||
.get_metadata(&absolute_path(file_path), /*sandbox*/ None)
|
||||
.get_metadata(&absolute_path(file_path.clone()), /*sandbox*/ None)
|
||||
.await
|
||||
.with_context(|| format!("mode={use_remote}"))?;
|
||||
assert_eq!(metadata.is_directory, false);
|
||||
assert_eq!(metadata.is_file, true);
|
||||
assert_eq!(metadata.is_symlink, false);
|
||||
assert!(metadata.modified_at_ms > 0);
|
||||
|
||||
let symlink_path = tmp.path().join("note-link.txt");
|
||||
symlink(&file_path, &symlink_path)?;
|
||||
let symlink_metadata = file_system
|
||||
.get_metadata(&absolute_path(symlink_path.clone()), /*sandbox*/ None)
|
||||
.await
|
||||
.with_context(|| format!("mode={use_remote}"))?;
|
||||
assert_eq!(symlink_metadata.is_directory, false);
|
||||
assert_eq!(symlink_metadata.is_file, true);
|
||||
assert_eq!(symlink_metadata.is_symlink, true);
|
||||
assert!(symlink_metadata.modified_at_ms > 0);
|
||||
|
||||
let dir_path = tmp.path().join("notes");
|
||||
std::fs::create_dir(&dir_path)?;
|
||||
let dir_symlink_path = tmp.path().join("notes-link");
|
||||
symlink(&dir_path, &dir_symlink_path)?;
|
||||
let dir_symlink_metadata = file_system
|
||||
.get_metadata(&absolute_path(dir_symlink_path), /*sandbox*/ None)
|
||||
.await
|
||||
.with_context(|| format!("mode={use_remote}"))?;
|
||||
assert_eq!(dir_symlink_metadata.is_directory, true);
|
||||
assert_eq!(dir_symlink_metadata.is_file, false);
|
||||
assert_eq!(dir_symlink_metadata.is_symlink, true);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user