use super::actions::SFTP_OPERATION_EVENT;
use crate::llm::action_helper::call_llm;
use crate::llm::ollama_client::OllamaClient;
use crate::protocol::Event;
use crate::server::connection::ConnectionId;
use crate::server::SshProtocol;
use crate::state::app_state::AppState;
use crate::utils::WireFailure;
use anyhow::Result;
use russh_sftp::protocol::*;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::{mpsc, Mutex};
use tokio_util::sync::CancellationToken;
use tracing::{debug, error, info, trace};
pub struct LlmSftpHandler {
connection_id: ConnectionId,
server_id: crate::state::ServerId,
llm_client: OllamaClient,
app_state: Arc<AppState>,
protocol: Arc<SshProtocol>,
status_tx: mpsc::UnboundedSender<String>,
handles: Arc<Mutex<HashMap<String, HandleInfo>>>,
version: Option<u32>,
owner: CancellationToken,
}
fn is_sftp_reply_action(name: &str) -> bool {
matches!(
name,
"sftp_handle"
| "sftp_directory_listing"
| "sftp_file_content"
| "sftp_file_attributes"
| "sftp_error"
)
}
fn sftp_error_status(response: &serde_json::Value) -> Option<StatusCode> {
if response.get("type").and_then(|v| v.as_str()) != Some("sftp_error") {
return None;
}
let code = response
.get("code")
.and_then(|v| v.as_str())
.unwrap_or("no_such_file");
Some(match code {
"no_such_file" => StatusCode::NoSuchFile,
"permission_denied" => StatusCode::PermissionDenied,
"op_unsupported" => StatusCode::OpUnsupported,
"eof" => StatusCode::Eof,
_ => StatusCode::Failure,
})
}
const BACKEND_FAILURE_STATUS: StatusCode = StatusCode::Failure;
fn sftp_no_answer(response: &serde_json::Value) -> bool {
let Some(obj) = response.as_object() else {
return true;
};
if obj.is_empty() {
return true;
}
match obj.get("type").and_then(|v| v.as_str()) {
Some(name) => !is_sftp_reply_action(name),
None => false,
}
}
#[derive(Debug, Clone)]
struct HandleInfo {
path: String,
#[allow(dead_code)] is_directory: bool,
dir_read_done: bool,
}
impl LlmSftpHandler {
pub fn new(
connection_id: ConnectionId,
server_id: crate::state::ServerId,
llm_client: OllamaClient,
app_state: Arc<AppState>,
protocol: Arc<SshProtocol>,
status_tx: mpsc::UnboundedSender<String>,
owner: CancellationToken,
) -> Self {
Self {
connection_id,
server_id,
llm_client,
app_state,
protocol,
status_tx,
handles: Arc::new(Mutex::new(HashMap::new())),
version: None,
owner,
}
}
async fn llm_sftp_operation(
&self,
operation: &str,
params: serde_json::Value,
) -> Result<serde_json::Value> {
debug!(
"SFTP LLM request: operation={}, params={}",
operation, params
);
let _ = self.status_tx.send(format!(
"[DEBUG] SFTP LLM request: operation={}, params={}",
operation, params
));
let mut event_data = serde_json::json!({ "operation": operation });
if let Some(fields) = params.as_object() {
for (key, value) in fields {
event_data[key] = value.clone();
}
}
let event = Event::new(&SFTP_OPERATION_EVENT, event_data);
trace!("SFTP calling LLM for operation: {}", operation);
let _ = self.status_tx.send(format!(
"[TRACE] SFTP calling LLM for operation: {}",
operation
));
match super::until_owner_drops(
&self.owner,
call_llm(
&self.llm_client,
&self.app_state,
self.server_id,
Some(self.connection_id),
&event,
self.protocol.as_ref(),
),
)
.await
{
Ok(execution_result) => {
for message in &execution_result.messages {
info!("{}", message);
let _ = self.status_tx.send(format!("[INFO] {}", message));
}
debug!(
"SFTP LLM returned {} actions for operation: {}",
execution_result.raw_actions.len(),
operation
);
let _ = self.status_tx.send(format!(
"[DEBUG] SFTP LLM returned {} actions for operation: {}",
execution_result.raw_actions.len(),
operation
));
if !execution_result.raw_actions.is_empty() {
let pretty = serde_json::to_string_pretty(&execution_result.raw_actions[0])
.unwrap_or_else(|_| format!("{:?}", execution_result.raw_actions[0]));
trace!("SFTP LLM response ({}) JSON:\n{}", operation, pretty);
let _ = self.status_tx.send(format!(
"[TRACE] SFTP LLM response ({}) JSON:\r\n{}",
operation,
pretty.replace('\n', "\r\n")
));
}
let reply = execution_result
.raw_actions
.iter()
.find(|a| {
a.get("type")
.and_then(|v| v.as_str())
.is_some_and(is_sftp_reply_action)
})
.or_else(|| execution_result.raw_actions.first())
.cloned()
.unwrap_or_else(|| serde_json::json!({}));
Ok(reply)
}
Err(e) => {
let failure = WireFailure::classify(&e);
error!(
"SFTP {}: decision=fail_closed_backend_error category={:?}: {}",
operation, failure, e
);
let _ = self.status_tx.send(format!(
"[ERROR] SFTP {}: decision=fail_closed_backend_error category={:?}",
operation, failure
));
Err(e)
}
}
}
}
impl russh_sftp::server::Handler for LlmSftpHandler {
type Error = StatusCode;
async fn init(
&mut self,
version: u32,
extensions: HashMap<String, String>,
) -> Result<Version, Self::Error> {
info!(
"SFTP init: version={}, extensions={:?}",
version, extensions
);
self.version = Some(version);
let _ = self
.status_tx
.send(format!("SFTP session initialized (v{})", version));
Ok(Version::new())
}
async fn opendir(&mut self, id: u32, path: String) -> Result<Handle, Self::Error> {
debug!("SFTP request: SSH_FXP_OPENDIR id={}, path={}", id, path);
let _ = self.status_tx.send(format!(
"[DEBUG] SFTP request: SSH_FXP_OPENDIR id={}, path={}",
id, path
));
trace!("SFTP SSH_FXP_OPENDIR request: id={}, path='{}'", id, path);
let _ = self.status_tx.send(format!(
"[TRACE] SFTP SSH_FXP_OPENDIR request: id={}, path='{}'",
id, path
));
let params = serde_json::json!({ "path": path });
match self.llm_sftp_operation("opendir", params).await {
Ok(response) => {
if let Some(status) = sftp_error_status(&response) {
debug!("SFTP opendir refused by handler for {}: {:?}", path, status);
let _ = self.status_tx.send(format!(
"[DEBUG] SFTP opendir for {path}: decision=model_reject"
));
return Err(status);
}
if sftp_no_answer(&response) {
error!("SFTP opendir for {}: decision=fail_closed_no_answer", path);
let _ = self.status_tx.send(format!(
"[ERROR] SFTP opendir for {path}: decision=fail_closed_no_answer"
));
return Err(BACKEND_FAILURE_STATUS);
}
let handle_str = response
.get("handle")
.and_then(|v| v.as_str())
.unwrap_or(&path)
.to_string();
self.handles.lock().await.insert(
handle_str.clone(),
HandleInfo {
path: path.clone(),
is_directory: true,
dir_read_done: false,
},
);
let _ = self
.status_tx
.send(format!("→ SFTP opened directory: {}", path));
debug!(
"SFTP response: SSH_FXP_HANDLE id={}, handle_len={} bytes",
id,
handle_str.len()
);
let _ = self.status_tx.send(format!(
"[DEBUG] SFTP response: SSH_FXP_HANDLE id={}, handle_len={} bytes",
id,
handle_str.len()
));
trace!(
"SFTP SSH_FXP_HANDLE response: id={}, handle='{}'",
id,
handle_str
);
let _ = self.status_tx.send(format!(
"[TRACE] SFTP SSH_FXP_HANDLE response: id={}, handle='{}'",
id, handle_str
));
Ok(Handle {
id,
handle: handle_str,
})
}
Err(_) => {
debug!("SFTP response: SSH_FXP_STATUS id={}, status=FAILURE", id);
let _ = self.status_tx.send(format!(
"[DEBUG] SFTP response: SSH_FXP_STATUS id={}, status=FAILURE (opendir '{}')",
id, path
));
Err(BACKEND_FAILURE_STATUS)
}
}
}
async fn readdir(&mut self, id: u32, handle: String) -> Result<Name, Self::Error> {
debug!(
"SFTP request: SSH_FXP_READDIR id={}, handle_len={} bytes",
id,
handle.len()
);
let _ = self.status_tx.send(format!(
"[DEBUG] SFTP request: SSH_FXP_READDIR id={}, handle_len={} bytes",
id,
handle.len()
));
trace!(
"SFTP SSH_FXP_READDIR request: id={}, handle='{}'",
id,
handle
);
let _ = self.status_tx.send(format!(
"[TRACE] SFTP SSH_FXP_READDIR request: id={}, handle='{}'",
id, handle
));
let mut handles = self.handles.lock().await;
if let Some(handle_info) = handles.get_mut(&handle) {
if handle_info.dir_read_done {
debug!(
"SFTP response: SSH_FXP_STATUS id={}, status=EOF (directory already read)",
id
);
let _ = self.status_tx.send(format!("[DEBUG] SFTP response: SSH_FXP_STATUS id={}, status=EOF (directory already read)", id));
return Err(StatusCode::Eof);
}
let path = handle_info.path.clone();
drop(handles);
let params = serde_json::json!({ "path": path, "handle": handle });
match self.llm_sftp_operation("readdir", params).await {
Ok(response) => {
if let Some(status) = sftp_error_status(&response) {
debug!(
"SFTP readdir for {}: decision=model_reject ({:?})",
path, status
);
return Err(status);
}
if sftp_no_answer(&response) {
error!("SFTP readdir for {}: decision=fail_closed_no_answer", path);
let _ = self.status_tx.send(format!(
"[ERROR] SFTP readdir for {path}: decision=fail_closed_no_answer"
));
return Err(BACKEND_FAILURE_STATUS);
}
let files = response
.get("entries")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|entry| {
let name = entry.get("name")?.as_str()?.to_string();
let is_dir = entry
.get("is_dir")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let size =
entry.get("size").and_then(|v| v.as_u64()).unwrap_or(0);
let attrs = FileAttributes {
size: Some(size),
permissions: Some(if is_dir { 0o40755 } else { 0o100644 }),
..Default::default()
};
Some(File::new(name, attrs))
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
if let Some(h) = self.handles.lock().await.get_mut(&handle) {
h.dir_read_done = true;
}
let _ = self.status_tx.send(format!(
"→ SFTP listed {} items in {}",
files.len(),
path
));
debug!(
"SFTP response: SSH_FXP_NAME id={}, file_count={}",
id,
files.len()
);
let _ = self.status_tx.send(format!(
"[DEBUG] SFTP response: SSH_FXP_NAME id={}, file_count={}",
id,
files.len()
));
let file_names: Vec<&str> = files.iter().map(|f| f.filename.as_str()).collect();
trace!(
"SFTP SSH_FXP_NAME response: id={}, files={:?}",
id,
file_names
);
let _ = self.status_tx.send(format!(
"[TRACE] SFTP SSH_FXP_NAME response: id={}, files={:?}",
id, file_names
));
Ok(Name { id, files })
}
Err(_) => {
debug!("SFTP response: SSH_FXP_STATUS id={}, status=FAILURE", id);
let _ = self.status_tx.send(format!(
"[DEBUG] SFTP response: SSH_FXP_STATUS id={}, status=FAILURE (readdir '{}')",
id, handle
));
Err(BACKEND_FAILURE_STATUS)
}
}
} else {
error!("SFTP readdir: invalid handle {}", handle);
let _ = self
.status_tx
.send(format!("[ERROR] SFTP readdir: invalid handle {}", handle));
debug!(
"SFTP response: SSH_FXP_STATUS id={}, status=BAD_MESSAGE",
id
);
let _ = self.status_tx.send(format!(
"[DEBUG] SFTP response: SSH_FXP_STATUS id={}, status=BAD_MESSAGE",
id
));
Err(StatusCode::BadMessage)
}
}
async fn open(
&mut self,
id: u32,
path: String,
pflags: OpenFlags,
_attrs: FileAttributes,
) -> Result<Handle, Self::Error> {
debug!(
"SFTP request: SSH_FXP_OPEN id={}, path={}, flags={:?}",
id, path, pflags
);
let _ = self.status_tx.send(format!(
"[DEBUG] SFTP request: SSH_FXP_OPEN id={}, path={}, flags={:?}",
id, path, pflags
));
trace!(
"SFTP SSH_FXP_OPEN request: id={}, path='{}', flags={:?}",
id,
path,
pflags
);
let _ = self.status_tx.send(format!(
"[TRACE] SFTP SSH_FXP_OPEN request: id={}, path='{}', flags={:?}",
id, path, pflags
));
let params = serde_json::json!({ "path": path });
match self.llm_sftp_operation("open", params).await {
Ok(response) => {
if let Some(status) = sftp_error_status(&response) {
debug!("SFTP open refused by handler for {}: {:?}", path, status);
let _ = self.status_tx.send(format!(
"[DEBUG] SFTP open for {path}: decision=model_reject"
));
return Err(status);
}
if sftp_no_answer(&response) {
error!("SFTP open for {}: decision=fail_closed_no_answer", path);
let _ = self.status_tx.send(format!(
"[ERROR] SFTP open for {path}: decision=fail_closed_no_answer"
));
return Err(BACKEND_FAILURE_STATUS);
}
let handle_str = response
.get("handle")
.and_then(|v| v.as_str())
.unwrap_or(&path)
.to_string();
self.handles.lock().await.insert(
handle_str.clone(),
HandleInfo {
path: path.clone(),
is_directory: false,
dir_read_done: false,
},
);
let _ = self.status_tx.send(format!("→ SFTP opened file: {}", path));
debug!(
"SFTP response: SSH_FXP_HANDLE id={}, handle_len={} bytes",
id,
handle_str.len()
);
let _ = self.status_tx.send(format!(
"[DEBUG] SFTP response: SSH_FXP_HANDLE id={}, handle_len={} bytes",
id,
handle_str.len()
));
trace!(
"SFTP SSH_FXP_HANDLE response: id={}, handle='{}'",
id,
handle_str
);
let _ = self.status_tx.send(format!(
"[TRACE] SFTP SSH_FXP_HANDLE response: id={}, handle='{}'",
id, handle_str
));
Ok(Handle {
id,
handle: handle_str,
})
}
Err(_) => {
debug!("SFTP response: SSH_FXP_STATUS id={}, status=FAILURE", id);
let _ = self.status_tx.send(format!(
"[DEBUG] SFTP response: SSH_FXP_STATUS id={}, status=FAILURE (open '{}')",
id, path
));
Err(BACKEND_FAILURE_STATUS)
}
}
}
async fn read(
&mut self,
id: u32,
handle: String,
offset: u64,
len: u32,
) -> Result<Data, Self::Error> {
debug!(
"SFTP request: SSH_FXP_READ id={}, offset={}, len={} bytes",
id, offset, len
);
let _ = self.status_tx.send(format!(
"[DEBUG] SFTP request: SSH_FXP_READ id={}, offset={}, len={} bytes",
id, offset, len
));
trace!(
"SFTP SSH_FXP_READ request: id={}, handle='{}', offset={}, len={}",
id,
handle,
offset,
len
);
let _ = self.status_tx.send(format!(
"[TRACE] SFTP SSH_FXP_READ request: id={}, handle='{}', offset={}, len={}",
id, handle, offset, len
));
let handles = self.handles.lock().await;
if let Some(handle_info) = handles.get(&handle) {
let path = handle_info.path.clone();
drop(handles);
let params = serde_json::json!({
"path": path,
"handle": handle,
"offset": offset,
"length": len,
});
match self.llm_sftp_operation("read", params).await {
Ok(response) => {
if let Some(status) = sftp_error_status(&response) {
debug!(
"SFTP read for {}: decision=model_reject ({:?})",
path, status
);
return Err(status);
}
if sftp_no_answer(&response) {
error!("SFTP read for {}: decision=fail_closed_no_answer", path);
let _ = self.status_tx.send(format!(
"[ERROR] SFTP read for {path}: decision=fail_closed_no_answer"
));
return Err(BACKEND_FAILURE_STATUS);
}
let content = response
.get("content")
.and_then(|v| v.as_str())
.unwrap_or("");
let full = content.as_bytes();
let start = usize::try_from(offset).unwrap_or(usize::MAX);
if start >= full.len() {
debug!(
"SFTP response: SSH_FXP_STATUS id={}, status=EOF (offset {} >= size {})",
id,
offset,
full.len()
);
return Err(StatusCode::Eof);
}
let end = start.saturating_add(len as usize).min(full.len());
let data = full[start..end].to_vec();
let actual_len = data.len();
let _ = self
.status_tx
.send(format!("→ SFTP read {} bytes from {}", actual_len, path));
debug!(
"SFTP response: SSH_FXP_DATA id={}, data_len={} bytes",
id, actual_len
);
let _ = self.status_tx.send(format!(
"[DEBUG] SFTP response: SSH_FXP_DATA id={}, data_len={} bytes",
id, actual_len
));
if actual_len <= 256 {
trace!(
"SFTP SSH_FXP_DATA response: id={}, data={:?}",
id,
String::from_utf8_lossy(&data)
);
let _ = self.status_tx.send(format!(
"[TRACE] SFTP SSH_FXP_DATA response: id={}, data={:?}",
id,
String::from_utf8_lossy(&data)
));
} else {
trace!(
"SFTP SSH_FXP_DATA response: id={}, data_len={} bytes (truncated)",
id,
actual_len
);
let _ = self.status_tx.send(format!("[TRACE] SFTP SSH_FXP_DATA response: id={}, data_len={} bytes (truncated)", id, actual_len));
}
Ok(Data { id, data })
}
Err(_) => {
debug!("SFTP response: SSH_FXP_STATUS id={}, status=FAILURE", id);
let _ = self.status_tx.send(format!(
"[DEBUG] SFTP response: SSH_FXP_STATUS id={}, status=FAILURE (read '{}')",
id, handle
));
Err(BACKEND_FAILURE_STATUS)
}
}
} else {
error!("SFTP read: invalid handle {}", handle);
let _ = self
.status_tx
.send(format!("[ERROR] SFTP read: invalid handle {}", handle));
debug!(
"SFTP response: SSH_FXP_STATUS id={}, status=BAD_MESSAGE",
id
);
let _ = self.status_tx.send(format!(
"[DEBUG] SFTP response: SSH_FXP_STATUS id={}, status=BAD_MESSAGE",
id
));
Err(StatusCode::BadMessage)
}
}
async fn close(&mut self, id: u32, handle: String) -> Result<Status, Self::Error> {
debug!(
"SFTP request: SSH_FXP_CLOSE id={}, handle_len={} bytes",
id,
handle.len()
);
let _ = self.status_tx.send(format!(
"[DEBUG] SFTP request: SSH_FXP_CLOSE id={}, handle_len={} bytes",
id,
handle.len()
));
trace!("SFTP SSH_FXP_CLOSE request: id={}, handle='{}'", id, handle);
let _ = self.status_tx.send(format!(
"[TRACE] SFTP SSH_FXP_CLOSE request: id={}, handle='{}'",
id, handle
));
if let Some(handle_info) = self.handles.lock().await.remove(&handle) {
let _ = self
.status_tx
.send(format!("→ SFTP closed: {}", handle_info.path));
debug!(
"SFTP response: SSH_FXP_STATUS id={}, status=OK (closed '{}')",
id, handle_info.path
);
let _ = self.status_tx.send(format!(
"[DEBUG] SFTP response: SSH_FXP_STATUS id={}, status=OK (closed '{}')",
id, handle_info.path
));
trace!(
"SFTP SSH_FXP_STATUS response: id={}, status=OK, path='{}'",
id,
handle_info.path
);
let _ = self.status_tx.send(format!(
"[TRACE] SFTP SSH_FXP_STATUS response: id={}, status=OK, path='{}'",
id, handle_info.path
));
} else {
debug!(
"SFTP response: SSH_FXP_STATUS id={}, status=OK (unknown handle)",
id
);
let _ = self.status_tx.send(format!(
"[DEBUG] SFTP response: SSH_FXP_STATUS id={}, status=OK (unknown handle)",
id
));
}
Ok(Status {
id,
status_code: StatusCode::Ok,
error_message: "".to_string(),
language_tag: "en-US".to_string(),
})
}
async fn lstat(&mut self, id: u32, path: String) -> Result<Attrs, Self::Error> {
debug!("SFTP request: SSH_FXP_LSTAT id={}, path={}", id, path);
let _ = self.status_tx.send(format!(
"[DEBUG] SFTP request: SSH_FXP_LSTAT id={}, path={}",
id, path
));
trace!("SFTP SSH_FXP_LSTAT request: id={}, path='{}'", id, path);
let _ = self.status_tx.send(format!(
"[TRACE] SFTP SSH_FXP_LSTAT request: id={}, path='{}'",
id, path
));
let params = serde_json::json!({ "path": path });
match self.llm_sftp_operation("lstat", params).await {
Ok(response) => {
if let Some(status) = sftp_error_status(&response) {
debug!("SFTP lstat refused by handler for {}: {:?}", path, status);
let _ = self.status_tx.send(format!(
"[DEBUG] SFTP lstat for {path}: decision=model_reject"
));
return Err(status);
}
if sftp_no_answer(&response) {
error!("SFTP lstat for {}: decision=fail_closed_no_answer", path);
let _ = self.status_tx.send(format!(
"[ERROR] SFTP lstat for {path}: decision=fail_closed_no_answer"
));
return Err(BACKEND_FAILURE_STATUS);
}
let mut attrs = FileAttributes::default();
if let Some(size) = response.get("size").and_then(|v| v.as_u64()) {
attrs.size = Some(size);
}
let is_dir = response
.get("is_dir")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let type_bit = if is_dir { 0o40000 } else { 0o100000 };
attrs.permissions =
Some(match response.get("permissions").and_then(|v| v.as_u64()) {
Some(perms) if perms & 0o170000 != 0 => perms as u32,
Some(perms) => perms as u32 | type_bit,
None if is_dir => 0o40755,
None => 0o100644,
});
debug!(
"SFTP response: SSH_FXP_ATTRS id={}, size={:?}, perms={:?}",
id, attrs.size, attrs.permissions
);
let _ = self.status_tx.send(format!(
"[DEBUG] SFTP response: SSH_FXP_ATTRS id={}, size={:?}, perms={:?}",
id, attrs.size, attrs.permissions
));
trace!(
"SFTP SSH_FXP_ATTRS response: id={}, path='{}', attrs={:?}",
id,
path,
attrs
);
let _ = self.status_tx.send(format!(
"[TRACE] SFTP SSH_FXP_ATTRS response: id={}, path='{}', attrs={:?}",
id, path, attrs
));
Ok(Attrs { id, attrs })
}
Err(_) => {
debug!("SFTP response: SSH_FXP_STATUS id={}, status=FAILURE", id);
let _ = self.status_tx.send(format!(
"[DEBUG] SFTP response: SSH_FXP_STATUS id={}, status=FAILURE (lstat '{}')",
id, path
));
Err(BACKEND_FAILURE_STATUS)
}
}
}
async fn fstat(&mut self, id: u32, handle: String) -> Result<Attrs, Self::Error> {
debug!(
"SFTP request: SSH_FXP_FSTAT id={}, handle_len={} bytes",
id,
handle.len()
);
let _ = self.status_tx.send(format!(
"[DEBUG] SFTP request: SSH_FXP_FSTAT id={}, handle_len={} bytes",
id,
handle.len()
));
trace!("SFTP SSH_FXP_FSTAT request: id={}, handle='{}'", id, handle);
let _ = self.status_tx.send(format!(
"[TRACE] SFTP SSH_FXP_FSTAT request: id={}, handle='{}'",
id, handle
));
let handles = self.handles.lock().await;
if let Some(handle_info) = handles.get(&handle) {
let path = handle_info.path.clone();
drop(handles);
trace!("SFTP fstat delegating to lstat for path: '{}'", path);
let _ = self.status_tx.send(format!(
"[TRACE] SFTP fstat delegating to lstat for path: '{}'",
path
));
self.lstat(id, path).await
} else {
error!("SFTP fstat: invalid handle {}", handle);
let _ = self
.status_tx
.send(format!("[ERROR] SFTP fstat: invalid handle {}", handle));
debug!(
"SFTP response: SSH_FXP_STATUS id={}, status=BAD_MESSAGE",
id
);
let _ = self.status_tx.send(format!(
"[DEBUG] SFTP response: SSH_FXP_STATUS id={}, status=BAD_MESSAGE",
id
));
Err(StatusCode::BadMessage)
}
}
async fn stat(&mut self, id: u32, path: String) -> Result<Attrs, Self::Error> {
debug!("SFTP request: SSH_FXP_STAT id={}, path={}", id, path);
let _ = self.status_tx.send(format!(
"[DEBUG] SFTP request: SSH_FXP_STAT id={}, path={}",
id, path
));
trace!("SFTP stat delegating to lstat for path: '{}'", path);
self.lstat(id, path).await
}
async fn realpath(&mut self, id: u32, path: String) -> Result<Name, Self::Error> {
debug!("SFTP request: SSH_FXP_REALPATH id={}, path={}", id, path);
let _ = self.status_tx.send(format!(
"[DEBUG] SFTP request: SSH_FXP_REALPATH id={}, path={}",
id, path
));
trace!("SFTP SSH_FXP_REALPATH request: id={}, path='{}'", id, path);
let _ = self.status_tx.send(format!(
"[TRACE] SFTP SSH_FXP_REALPATH request: id={}, path='{}'",
id, path
));
let path = if path.is_empty() || path == "." {
"/".to_string()
} else {
path
};
let attrs = FileAttributes::default();
let file = File::new(path.clone(), attrs);
debug!(
"SFTP response: SSH_FXP_NAME id={}, resolved_path={}",
id, path
);
let _ = self.status_tx.send(format!(
"[DEBUG] SFTP response: SSH_FXP_NAME id={}, resolved_path={}",
id, path
));
trace!("SFTP SSH_FXP_NAME response: id={}, path='{}'", id, path);
let _ = self.status_tx.send(format!(
"[TRACE] SFTP SSH_FXP_NAME response: id={}, path='{}'",
id, path
));
Ok(Name {
id,
files: vec![file],
})
}
fn unimplemented(&self) -> Self::Error {
error!("SFTP unimplemented packet received");
let _ = self
.status_tx
.send("[ERROR] SFTP unimplemented packet received".to_string());
StatusCode::OpUnsupported
}
}