use crate::DriverCallback;
use crate::DriverContext;
use crate::{Driver, DriverCategory, DriverParameter};
use anyhow::Result;
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};
fn get_param_string(params: &HashMap<String, Value>, name: &str) -> Result<String> {
params
.get(name)
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.ok_or_else(|| anyhow::anyhow!("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 {
DriverCategory::Network
}
fn parameters(&self) -> Vec<DriverParameter> {
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) -> Value {
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 {
"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>,
) -> Result<String> {
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");
if !Path::new(&local_path).exists() {
anyhow::bail!("Local file not found: {}", local_path);
}
let file_size = fs::metadata(&local_path)?.len();
let addr_str = format!("{}:{}", host, port);
let addr = addr_str
.to_socket_addrs()?
.next()
.ok_or_else(|| anyhow::anyhow!("Invalid address: {}", addr_str))?;
let mut ftp = FtpStream::connect_timeout(addr, std::time::Duration::from_secs(timeout))?;
ftp.login(&username, &password)?;
if mode == "binary" {
ftp.transfer_type(FileType::Binary)?;
} else {
ftp.transfer_type(FileType::Ascii(FormatControl::NonPrint))?;
}
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)?;
ftp.put_file(&remote_path, &mut file)?;
ftp.quit()?;
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 {
DriverCategory::Network
}
fn parameters(&self) -> Vec<DriverParameter> {
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) -> Value {
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 {
"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>,
) -> Result<String> {
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);
let addr_str = format!("{}:{}", host, port);
let addr = addr_str
.to_socket_addrs()?
.next()
.ok_or_else(|| anyhow::anyhow!("Invalid address: {}", addr_str))?;
let mut ftp = FtpStream::connect_timeout(addr, std::time::Duration::from_secs(timeout))?;
ftp.login(&username, &password)?;
if let Some(parent) = Path::new(&local_path).parent() {
if !parent.exists() {
fs::create_dir_all(parent)?;
}
}
let mut stream = ftp.retr_as_stream(&remote_path)?;
let mut content = Vec::new();
std::io::copy(&mut stream, &mut content)?;
fs::write(&local_path, content)?;
let file_size = fs::metadata(&local_path)?.len();
ftp.quit()?;
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 {
DriverCategory::Network
}
fn parameters(&self) -> Vec<DriverParameter> {
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) -> Value {
json!({ "action": "ftp_list", "parameters": { "host": "ftp.example.com", "username": "user", "password": "pass", "directory": "/uploads" } })
}
fn example_output(&self) -> String {
"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>,
) -> Result<String> {
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);
let addr_str = format!("{}:{}", host, port);
let addr = addr_str
.to_socket_addrs()?
.next()
.ok_or_else(|| anyhow::anyhow!("Invalid address: {}", addr_str))?;
let mut ftp = FtpStream::connect_timeout(addr, std::time::Duration::from_secs(timeout))?;
ftp.login(&username, &password)?;
let listing = ftp.list(Some(directory))?;
let mut result = format!("Directory listing for {}:\n", directory);
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()?;
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 {
DriverCategory::Network
}
fn parameters(&self) -> Vec<DriverParameter> {
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) -> Value {
json!({ "action": "ftp_delete", "parameters": { "host": "ftp.example.com", "username": "user", "password": "pass", "remote_path": "/uploads/old_file.txt" } })
}
fn example_output(&self) -> String {
"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>,
) -> Result<String> {
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);
let addr_str = format!("{}:{}", host, port);
let addr = addr_str
.to_socket_addrs()?
.next()
.ok_or_else(|| anyhow::anyhow!("Invalid address: {}", addr_str))?;
let mut ftp = FtpStream::connect_timeout(addr, std::time::Duration::from_secs(timeout))?;
ftp.login(&username, &password)?;
ftp.rm(&remote_path)?;
ftp.quit()?;
Ok(format!(
"Successfully deleted {} from {}:{}",
remote_path, host, port
))
}
}