use crate::error_code::internal_error;use crate::error_code::invalid_request;use crate::fs_watch::FsWatchManager;use crate::outgoing_message::ConnectionId;use base64::Engine;use base64::engine::general_purpose::STANDARD;use codex_app_server_protocol::FsCopyParams;use codex_app_server_protocol::FsCopyResponse;use codex_app_server_protocol::FsCreateDirectoryParams;use codex_app_server_protocol::FsCreateDirectoryResponse;use codex_app_server_protocol::FsGetMetadataParams;use codex_app_server_protocol::FsGetMetadataResponse;use codex_app_server_protocol::FsReadDirectoryEntry;use codex_app_server_protocol::FsReadDirectoryParams;use codex_app_server_protocol::FsReadDirectoryResponse;use codex_app_server_protocol::FsReadFileParams;use codex_app_server_protocol::FsReadFileResponse;use codex_app_server_protocol::FsRemoveParams;use codex_app_server_protocol::FsRemoveResponse;use codex_app_server_protocol::FsUnwatchParams;use codex_app_server_protocol::FsUnwatchResponse;use codex_app_server_protocol::FsWatchParams;use codex_app_server_protocol::FsWatchResponse;use codex_app_server_protocol::FsWriteFileParams;use codex_app_server_protocol::FsWriteFileResponse;use codex_app_server_protocol::JSONRPCErrorError;use codex_exec_server::CopyOptions;use codex_exec_server::CreateDirectoryOptions;use codex_exec_server::EnvironmentManager;use codex_exec_server::ExecutorFileSystem;use codex_exec_server::RemoveOptions;use codex_utils_path_uri::PathUri;use std::io;use std::sync::Arc;#[derive(Clone)]pub(crate) struct FsRequestProcessor { environment_manager: Arc<EnvironmentManager>, fs_watch_manager: FsWatchManager,}impl FsRequestProcessor { pub(crate) fn new( environment_manager: Arc<EnvironmentManager>, fs_watch_manager: FsWatchManager, ) -> Self { Self { environment_manager, fs_watch_manager, } } fn file_system(&self) -> Result<Arc<dyn ExecutorFileSystem>, JSONRPCErrorError> { self.environment_manager .try_local_environment() .map(|environment| environment.get_filesystem()) .ok_or_else(|| internal_error("local filesystem is not configured")) } pub(crate) async fn connection_closed(&self, connection_id: ConnectionId) { self.fs_watch_manager.connection_closed(connection_id).await; } pub(crate) async fn read_file( &self, params: FsReadFileParams, ) -> Result<FsReadFileResponse, JSONRPCErrorError> { let path = PathUri::from_abs_path(¶ms.path); let bytes = self .file_system()? .read_file(&path, /*sandbox*/ None) .await .map_err(map_fs_error)?; Ok(FsReadFileResponse { data_base64: STANDARD.encode(bytes), }) } pub(crate) async fn write_file( &self, params: FsWriteFileParams, ) -> Result<FsWriteFileResponse, JSONRPCErrorError> { let bytes = STANDARD.decode(params.data_base64).map_err(|err| { invalid_request(format!( "fs/writeFile requires valid base64 dataBase64: {err}" )) })?; let path = PathUri::from_abs_path(¶ms.path); self.file_system()? .write_file(&path, bytes, /*sandbox*/ None) .await .map_err(map_fs_error)?; Ok(FsWriteFileResponse {}) } pub(crate) async fn create_directory( &self, params: FsCreateDirectoryParams, ) -> Result<FsCreateDirectoryResponse, JSONRPCErrorError> { let path = PathUri::from_abs_path(¶ms.path); self.file_system()? .create_directory( &path, CreateDirectoryOptions { recursive: params.recursive.unwrap_or(true), }, /*sandbox*/ None, ) .await .map_err(map_fs_error)?; Ok(FsCreateDirectoryResponse {}) } pub(crate) async fn get_metadata( &self, params: FsGetMetadataParams, ) -> Result<FsGetMetadataResponse, JSONRPCErrorError> { let path = PathUri::from_abs_path(¶ms.path); let metadata = self .file_system()? .get_metadata(&path, /*sandbox*/ None) .await .map_err(map_fs_error)?; 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, }) } pub(crate) async fn read_directory( &self, params: FsReadDirectoryParams, ) -> Result<FsReadDirectoryResponse, JSONRPCErrorError> { let path = PathUri::from_abs_path(¶ms.path); let entries = self .file_system()? .read_directory(&path, /*sandbox*/ None) .await .map_err(map_fs_error)?; Ok(FsReadDirectoryResponse { entries: entries .into_iter() .map(|entry| FsReadDirectoryEntry { file_name: entry.file_name, is_directory: entry.is_directory, is_file: entry.is_file, }) .collect(), }) } pub(crate) async fn remove( &self, params: FsRemoveParams, ) -> Result<FsRemoveResponse, JSONRPCErrorError> { let path = PathUri::from_abs_path(¶ms.path); self.file_system()? .remove( &path, RemoveOptions { recursive: params.recursive.unwrap_or(true), force: params.force.unwrap_or(true), }, /*sandbox*/ None, ) .await .map_err(map_fs_error)?; Ok(FsRemoveResponse {}) } pub(crate) async fn copy( &self, params: FsCopyParams, ) -> Result<FsCopyResponse, JSONRPCErrorError> { let source_path = PathUri::from_abs_path(¶ms.source_path); let destination_path = PathUri::from_abs_path(¶ms.destination_path); self.file_system()? .copy( &source_path, &destination_path, CopyOptions { recursive: params.recursive, }, /*sandbox*/ None, ) .await .map_err(map_fs_error)?; Ok(FsCopyResponse {}) } pub(crate) async fn watch( &self, connection_id: ConnectionId, params: FsWatchParams, ) -> Result<FsWatchResponse, JSONRPCErrorError> { self.file_system()?; self.fs_watch_manager.watch(connection_id, params).await } pub(crate) async fn unwatch( &self, connection_id: ConnectionId, params: FsUnwatchParams, ) -> Result<FsUnwatchResponse, JSONRPCErrorError> { self.file_system()?; self.fs_watch_manager.unwatch(connection_id, params).await }}fn map_fs_error(err: io::Error) -> JSONRPCErrorError { if err.kind() == io::ErrorKind::InvalidInput { invalid_request(err.to_string()) } else { internal_error(err.to_string()) }}