use crate::{
DriverCallback, DriverCategory, DriverContext, DriverError, DriverResult,
types::{Driver, DriverParameter},
};
use serde_json::{Value, json};
use std::collections::HashMap;
use std::fs;
use std::net::ToSocketAddrs;
use std::path::Path;
use suppaftp::types::{FileType, FormatControl};
use tracing::{debug, info};
fn get_param_string(params: &HashMap<String, Value>, name: &str) -> DriverResult<String> {
params.get(name).and_then(|v| v.as_str()).map(|s| s.to_string()).ok_or_else(|| DriverError::missing_parameter(name))
}
fn get_param_u64(params: &HashMap<String, Value>, name: &str, default: u64) -> u64 {
params.get(name).and_then(|v| v.as_u64()).unwrap_or(default)
}
#[derive(Debug)]
pub struct FtpUploadDriver;
#[async_trait::async_trait]
impl Driver for FtpUploadDriver {
fn name(&self) -> &str {
"ftp_upload"
}
fn description(&self) -> &str {
"Upload a file to FTP server"
}
fn usage_hint(&self) -> &str {
"Use this skill when the user needs to upload files to an FTP server"
}
fn category(&self) -> DriverCategory {
return DriverCategory::Network;
}
fn parameters(&self) -> Vec<DriverParameter> {
return vec![
DriverParameter {
name: "host".to_string(),
param_type: "string".to_string(),
description: "FTP server hostname or IP address".to_string(),
required: true,
default: None,
example: Some(Value::String("ftp.example.com".to_string())),
enum_values: None,
},
DriverParameter {
name: "port".to_string(),
param_type: "integer".to_string(),
description: "FTP server port".to_string(),
required: false,
default: Some(Value::Number(21.into())),
example: Some(Value::Number(21.into())),
enum_values: None,
},
DriverParameter {
name: "username".to_string(),
param_type: "string".to_string(),
description: "FTP username".to_string(),
required: true,
default: None,
example: Some(Value::String("user@example.com".to_string())),
enum_values: None,
},
DriverParameter {
name: "password".to_string(),
param_type: "string".to_string(),
description: "FTP password".to_string(),
required: true,
default: None,
example: Some(Value::String("secret123".to_string())),
enum_values: None,
},
DriverParameter {
name: "local_path".to_string(),
param_type: "string".to_string(),
description: "Local file path to upload".to_string(),
required: true,
default: None,
example: Some(Value::String("/home/user/document.pdf".to_string())),
enum_values: None,
},
DriverParameter {
name: "remote_path".to_string(),
param_type: "string".to_string(),
description: "Remote path where to upload the file".to_string(),
required: true,
default: None,
example: Some(Value::String("/uploads/document.pdf".to_string())),
enum_values: None,
},
DriverParameter {
name: "mode".to_string(),
param_type: "string".to_string(),
description: "Transfer mode (binary or ascii)".to_string(),
required: false,
default: Some(Value::String("binary".to_string())),
example: Some(Value::String("binary".to_string())),
enum_values: Some(vec!["binary".to_string(), "ascii".to_string()]),
},
DriverParameter {
name: "timeout".to_string(),
param_type: "integer".to_string(),
description: "Connection timeout in seconds".to_string(),
required: false,
default: Some(Value::Number(30.into())),
example: Some(Value::Number(10.into())),
enum_values: None,
},
];
}
fn example_call(&self) -> DriverResult<Value> {
return Ok(json!({
"action": "ftp_upload",
"parameters": {
"host": "ftp.example.com",
"username": "user",
"password": "pass",
"local_path": "/tmp/file.txt",
"remote_path": "/uploads/file.txt"
}
}));
}
fn example_output(&self) -> String {
return "Successfully uploaded /tmp/file.txt to ftp.example.com:/uploads/file.txt (1024 bytes)".to_string();
}
async fn execute(
&self,
parameters: &HashMap<String, Value>,
_callback: Option<&dyn DriverCallback>,
_context: Option<&DriverContext>,
) -> DriverResult<String> {
debug!("Executing ftp_upload driver");
use suppaftp::FtpStream;
let host = get_param_string(parameters, "host")?;
let port = get_param_u64(parameters, "port", 21) as u16;
let username = get_param_string(parameters, "username")?;
let password = get_param_string(parameters, "password")?;
let local_path = get_param_string(parameters, "local_path")?;
let remote_path = get_param_string(parameters, "remote_path")?;
let timeout = get_param_u64(parameters, "timeout", 30);
let mode = parameters.get("mode").and_then(|v| v.as_str()).unwrap_or("binary");
info!("FTP upload: host={}, port={}, username={}, local={}, remote={}", host, port, username, local_path, remote_path);
if !Path::new(&local_path).exists() {
debug!("Local file not found: {}", local_path);
return Err(DriverError::execution(format!("Local file not found: {}", local_path)));
}
let file_size = fs::metadata(&local_path).map_err(|e| DriverError::execution(format!("Failed to get file metadata: {}", e)))?.len();
let addr_str = format!("{}:{}", host, port);
let addr = addr_str
.to_socket_addrs()
.map_err(|e| DriverError::execution(format!("Invalid address: {}", e)))?
.next()
.ok_or_else(|| DriverError::execution(format!("Invalid address: {}", addr_str)))?;
let mut ftp = FtpStream::connect_timeout(addr, std::time::Duration::from_secs(timeout))
.map_err(|e| DriverError::execution(format!("Failed to connect to FTP server: {}", e)))?;
ftp.login(&username, &password).map_err(|e| DriverError::execution(format!("FTP login failed: {}", e)))?;
if mode == "binary" {
ftp.transfer_type(FileType::Binary).map_err(|e| DriverError::execution(format!("Failed to set binary mode: {}", e)))?;
} else {
ftp.transfer_type(FileType::Ascii(FormatControl::NonPrint))
.map_err(|e| DriverError::execution(format!("Failed to set ascii mode: {}", e)))?;
}
if let Some(parent) = Path::new(&remote_path).parent() {
let parent_str = parent.to_string_lossy();
if !parent_str.is_empty() && parent_str != "/" {
let _ = ftp.mkdir(&parent_str);
}
}
let mut file = fs::File::open(&local_path).map_err(|e| DriverError::execution(format!("Failed to open local file: {}", e)))?;
ftp.put_file(&remote_path, &mut file).map_err(|e| DriverError::execution(format!("Failed to upload file: {}", e)))?;
ftp.quit().map_err(|e| DriverError::execution(format!("Failed to quit FTP session: {}", e)))?;
info!("FTP upload successful: {} bytes", file_size);
return Ok(format!("Successfully uploaded {} to {}:{}{} ({} bytes)", local_path, host, port, remote_path, file_size));
}
}
#[derive(Debug)]
pub struct FtpDownloadDriver;
#[async_trait::async_trait]
impl Driver for FtpDownloadDriver {
fn name(&self) -> &str {
"ftp_download"
}
fn description(&self) -> &str {
"Download a file from FTP server"
}
fn usage_hint(&self) -> &str {
"Use this skill when the user needs to download files from an FTP server"
}
fn category(&self) -> DriverCategory {
return DriverCategory::Network;
}
fn parameters(&self) -> Vec<DriverParameter> {
return vec![
DriverParameter {
name: "host".to_string(),
param_type: "string".to_string(),
description: "FTP server hostname or IP address".to_string(),
required: true,
default: None,
example: Some(Value::String("ftp.example.com".to_string())),
enum_values: None,
},
DriverParameter {
name: "port".to_string(),
param_type: "integer".to_string(),
description: "FTP server port".to_string(),
required: false,
default: Some(Value::Number(21.into())),
example: Some(Value::Number(21.into())),
enum_values: None,
},
DriverParameter {
name: "username".to_string(),
param_type: "string".to_string(),
description: "FTP username".to_string(),
required: true,
default: None,
example: Some(Value::String("user@example.com".to_string())),
enum_values: None,
},
DriverParameter {
name: "password".to_string(),
param_type: "string".to_string(),
description: "FTP password".to_string(),
required: true,
default: None,
example: Some(Value::String("secret123".to_string())),
enum_values: None,
},
DriverParameter {
name: "remote_path".to_string(),
param_type: "string".to_string(),
description: "Remote file path to download".to_string(),
required: true,
default: None,
example: Some(Value::String("/uploads/document.pdf".to_string())),
enum_values: None,
},
DriverParameter {
name: "local_path".to_string(),
param_type: "string".to_string(),
description: "Local path where to save the file".to_string(),
required: true,
default: None,
example: Some(Value::String("/home/user/document.pdf".to_string())),
enum_values: None,
},
DriverParameter {
name: "mode".to_string(),
param_type: "string".to_string(),
description: "Transfer mode (binary or ascii)".to_string(),
required: false,
default: Some(Value::String("binary".to_string())),
example: Some(Value::String("binary".to_string())),
enum_values: Some(vec!["binary".to_string(), "ascii".to_string()]),
},
DriverParameter {
name: "timeout".to_string(),
param_type: "integer".to_string(),
description: "Connection timeout in seconds".to_string(),
required: false,
default: Some(Value::Number(30.into())),
example: Some(Value::Number(10.into())),
enum_values: None,
},
];
}
fn example_call(&self) -> DriverResult<Value> {
return Ok(json!({
"action": "ftp_download",
"parameters": {
"host": "ftp.example.com",
"username": "user",
"password": "pass",
"remote_path": "/uploads/file.txt",
"local_path": "/tmp/file.txt"
}
}));
}
fn example_output(&self) -> String {
return "Successfully downloaded ftp.example.com:/uploads/file.txt to /tmp/file.txt (1024 bytes)".to_string();
}
async fn execute(
&self,
parameters: &HashMap<String, Value>,
_callback: Option<&dyn DriverCallback>,
_context: Option<&DriverContext>,
) -> DriverResult<String> {
debug!("Executing ftp_download driver");
use suppaftp::FtpStream;
let host = get_param_string(parameters, "host")?;
let port = get_param_u64(parameters, "port", 21) as u16;
let username = get_param_string(parameters, "username")?;
let password = get_param_string(parameters, "password")?;
let remote_path = get_param_string(parameters, "remote_path")?;
let local_path = get_param_string(parameters, "local_path")?;
let timeout = get_param_u64(parameters, "timeout", 30);
info!("FTP download: host={}, port={}, username={}, remote={}, local={}", host, port, username, remote_path, local_path);
let addr_str = format!("{}:{}", host, port);
let addr = addr_str
.to_socket_addrs()
.map_err(|e| DriverError::execution(format!("Invalid address: {}", e)))?
.next()
.ok_or_else(|| DriverError::execution(format!("Invalid address: {}", addr_str)))?;
let mut ftp = FtpStream::connect_timeout(addr, std::time::Duration::from_secs(timeout))
.map_err(|e| DriverError::execution(format!("Failed to connect to FTP server: {}", e)))?;
ftp.login(&username, &password).map_err(|e| DriverError::execution(format!("FTP login failed: {}", e)))?;
if let Some(parent) = Path::new(&local_path).parent() {
if !parent.exists() {
fs::create_dir_all(parent).map_err(|e| DriverError::execution(format!("Failed to create directory: {}", e)))?;
}
}
let mut stream = ftp.retr_as_stream(&remote_path).map_err(|e| DriverError::execution(format!("Failed to start download: {}", e)))?;
let mut content = Vec::new();
std::io::copy(&mut stream, &mut content).map_err(|e| DriverError::execution(format!("Failed to download file: {}", e)))?;
fs::write(&local_path, content).map_err(|e| DriverError::execution(format!("Failed to write file: {}", e)))?;
let file_size = fs::metadata(&local_path).map_err(|e| DriverError::execution(format!("Failed to get file metadata: {}", e)))?.len();
ftp.quit().map_err(|e| DriverError::execution(format!("Failed to quit FTP session: {}", e)))?;
info!("FTP download successful: {} bytes", file_size);
return Ok(format!("Successfully downloaded {}:{}{} to {} ({} bytes)", host, port, remote_path, local_path, file_size));
}
}
#[derive(Debug)]
pub struct FtpListDriver;
#[async_trait::async_trait]
impl Driver for FtpListDriver {
fn name(&self) -> &str {
"ftp_list"
}
fn description(&self) -> &str {
"List directory contents on FTP server"
}
fn usage_hint(&self) -> &str {
"Use this skill when the user needs to see what files are in an FTP directory"
}
fn category(&self) -> DriverCategory {
return DriverCategory::Network;
}
fn parameters(&self) -> Vec<DriverParameter> {
return vec![
DriverParameter {
name: "host".to_string(),
param_type: "string".to_string(),
description: "FTP server hostname or IP address".to_string(),
required: true,
default: None,
example: Some(Value::String("ftp.example.com".to_string())),
enum_values: None,
},
DriverParameter {
name: "port".to_string(),
param_type: "integer".to_string(),
description: "FTP server port".to_string(),
required: false,
default: Some(Value::Number(21.into())),
example: Some(Value::Number(21.into())),
enum_values: None,
},
DriverParameter {
name: "username".to_string(),
param_type: "string".to_string(),
description: "FTP username".to_string(),
required: true,
default: None,
example: Some(Value::String("user@example.com".to_string())),
enum_values: None,
},
DriverParameter {
name: "password".to_string(),
param_type: "string".to_string(),
description: "FTP password".to_string(),
required: true,
default: None,
example: Some(Value::String("secret123".to_string())),
enum_values: None,
},
DriverParameter {
name: "directory".to_string(),
param_type: "string".to_string(),
description: "Remote directory to list".to_string(),
required: false,
default: Some(Value::String("/".to_string())),
example: Some(Value::String("/uploads".to_string())),
enum_values: None,
},
DriverParameter {
name: "timeout".to_string(),
param_type: "integer".to_string(),
description: "Connection timeout in seconds".to_string(),
required: false,
default: Some(Value::Number(30.into())),
example: Some(Value::Number(10.into())),
enum_values: None,
},
];
}
fn example_call(&self) -> DriverResult<Value> {
return Ok(json!({
"action": "ftp_list",
"parameters": {
"host": "ftp.example.com",
"username": "user",
"password": "pass",
"directory": "/uploads"
}
}));
}
fn example_output(&self) -> String {
return "Directory listing for /uploads:\nfile1.txt (1024 bytes)\nfile2.pdf (51200 bytes)\nfolder/".to_string();
}
async fn execute(
&self,
parameters: &HashMap<String, Value>,
_callback: Option<&dyn DriverCallback>,
_context: Option<&DriverContext>,
) -> DriverResult<String> {
debug!("Executing ftp_list driver");
use suppaftp::FtpStream;
let host = get_param_string(parameters, "host")?;
let port = get_param_u64(parameters, "port", 21) as u16;
let username = get_param_string(parameters, "username")?;
let password = get_param_string(parameters, "password")?;
let directory = parameters.get("directory").and_then(|v| v.as_str()).unwrap_or("/");
let timeout = get_param_u64(parameters, "timeout", 30);
info!("FTP list: host={}, port={}, username={}, directory={}", host, port, username, directory);
let addr_str = format!("{}:{}", host, port);
let addr = addr_str
.to_socket_addrs()
.map_err(|e| DriverError::execution(format!("Invalid address: {}", e)))?
.next()
.ok_or_else(|| DriverError::execution(format!("Invalid address: {}", addr_str)))?;
let mut ftp = FtpStream::connect_timeout(addr, std::time::Duration::from_secs(timeout))
.map_err(|e| DriverError::execution(format!("Failed to connect to FTP server: {}", e)))?;
ftp.login(&username, &password).map_err(|e| DriverError::execution(format!("FTP login failed: {}", e)))?;
let listing = ftp.list(Some(directory)).map_err(|e| DriverError::execution(format!("Failed to list directory: {}", e)))?;
let mut result = format!("Directory listing for {}:\n", directory);
let listing_size = listing.len();
for line in listing {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 9 {
let size = parts[4];
let name = parts[8..].join(" ");
if line.starts_with('d') {
result.push_str(&format!("{}/\n", name));
} else {
result.push_str(&format!("{} ({} bytes)\n", name, size));
}
} else {
result.push_str(&format!("{}\n", line));
}
}
ftp.quit().map_err(|e| DriverError::execution(format!("Failed to quit FTP session: {}", e)))?;
info!("FTP list completed: {} entries found", listing_size);
return Ok(result.trim_end().to_string());
}
}
#[derive(Debug)]
pub struct FtpDeleteDriver;
#[async_trait::async_trait]
impl Driver for FtpDeleteDriver {
fn name(&self) -> &str {
"ftp_delete"
}
fn description(&self) -> &str {
"Delete a file from FTP server"
}
fn usage_hint(&self) -> &str {
"Use this skill when the user needs to delete files on an FTP server"
}
fn category(&self) -> DriverCategory {
return DriverCategory::Network;
}
fn parameters(&self) -> Vec<DriverParameter> {
return vec![
DriverParameter {
name: "host".to_string(),
param_type: "string".to_string(),
description: "FTP server hostname or IP address".to_string(),
required: true,
default: None,
example: Some(Value::String("ftp.example.com".to_string())),
enum_values: None,
},
DriverParameter {
name: "port".to_string(),
param_type: "integer".to_string(),
description: "FTP server port".to_string(),
required: false,
default: Some(Value::Number(21.into())),
example: Some(Value::Number(21.into())),
enum_values: None,
},
DriverParameter {
name: "username".to_string(),
param_type: "string".to_string(),
description: "FTP username".to_string(),
required: true,
default: None,
example: Some(Value::String("user@example.com".to_string())),
enum_values: None,
},
DriverParameter {
name: "password".to_string(),
param_type: "string".to_string(),
description: "FTP password".to_string(),
required: true,
default: None,
example: Some(Value::String("secret123".to_string())),
enum_values: None,
},
DriverParameter {
name: "remote_path".to_string(),
param_type: "string".to_string(),
description: "Remote file path to delete".to_string(),
required: true,
default: None,
example: Some(Value::String("/uploads/old_file.txt".to_string())),
enum_values: None,
},
DriverParameter {
name: "timeout".to_string(),
param_type: "integer".to_string(),
description: "Connection timeout in seconds".to_string(),
required: false,
default: Some(Value::Number(30.into())),
example: Some(Value::Number(10.into())),
enum_values: None,
},
];
}
fn example_call(&self) -> DriverResult<Value> {
return Ok(json!({
"action": "ftp_delete",
"parameters": {
"host": "ftp.example.com",
"username": "user",
"password": "pass",
"remote_path": "/uploads/old_file.txt"
}
}));
}
fn example_output(&self) -> String {
return "Successfully deleted /uploads/old_file.txt from ftp.example.com".to_string();
}
async fn execute(
&self,
parameters: &HashMap<String, Value>,
_callback: Option<&dyn DriverCallback>,
_context: Option<&DriverContext>,
) -> DriverResult<String> {
debug!("Executing ftp_delete driver");
use suppaftp::FtpStream;
let host = get_param_string(parameters, "host")?;
let port = get_param_u64(parameters, "port", 21) as u16;
let username = get_param_string(parameters, "username")?;
let password = get_param_string(parameters, "password")?;
let remote_path = get_param_string(parameters, "remote_path")?;
let timeout = get_param_u64(parameters, "timeout", 30);
info!("FTP delete: host={}, port={}, username={}, remote_path={}", host, port, username, remote_path);
let addr_str = format!("{}:{}", host, port);
let addr = addr_str
.to_socket_addrs()
.map_err(|e| DriverError::execution(format!("Invalid address: {}", e)))?
.next()
.ok_or_else(|| DriverError::execution(format!("Invalid address: {}", addr_str)))?;
let mut ftp = FtpStream::connect_timeout(addr, std::time::Duration::from_secs(timeout))
.map_err(|e| DriverError::execution(format!("Failed to connect to FTP server: {}", e)))?;
ftp.login(&username, &password).map_err(|e| DriverError::execution(format!("FTP login failed: {}", e)))?;
ftp.rm(&remote_path).map_err(|e| DriverError::execution(format!("Failed to delete file: {}", e)))?;
ftp.quit().map_err(|e| DriverError::execution(format!("Failed to quit FTP session: {}", e)))?;
info!("FTP delete successful: {}", remote_path);
return Ok(format!("Successfully deleted {} from {}:{}", remote_path, host, port));
}
}