use crate::{
BatchError,
core::step::{RepeatStatus, StepExecution, Tasklet},
};
use log::info;
use std::{
fs::{self, File},
io::BufReader,
path::{Path, PathBuf},
time::Duration,
};
use suppaftp::{FtpError, FtpStream, Mode};
#[cfg(feature = "ftp")]
use suppaftp::{NativeTlsConnector, NativeTlsFtpStream};
#[cfg(feature = "ftp")]
use suppaftp::native_tls::TlsConnector;
fn io_error_to_ftp_error(error: std::io::Error) -> FtpError {
FtpError::ConnectionError(error)
}
fn setup_ftp_connection(
host: &str,
port: u16,
username: &str,
password: &str,
passive_mode: bool,
timeout: Duration,
) -> Result<FtpStream, BatchError> {
let mut ftp_stream = FtpStream::connect(format!("{}:{}", host, port)).map_err(|e| {
BatchError::Io(std::io::Error::new(
std::io::ErrorKind::ConnectionRefused,
format!("Failed to connect to FTP server: {}", e),
))
})?;
ftp_stream
.login(username, password)
.map_err(|e| BatchError::Configuration(format!("FTP login failed: {}", e)))?;
ftp_stream
.get_ref()
.set_read_timeout(Some(timeout))
.map_err(|e| BatchError::Configuration(format!("Failed to set read timeout: {}", e)))?;
ftp_stream
.get_ref()
.set_write_timeout(Some(timeout))
.map_err(|e| BatchError::Configuration(format!("Failed to set write timeout: {}", e)))?;
let mode = if passive_mode {
Mode::Passive
} else {
Mode::Active
};
ftp_stream.set_mode(mode);
Ok(ftp_stream)
}
#[cfg(feature = "ftp")]
fn setup_ftps_connection(
host: &str,
port: u16,
username: &str,
password: &str,
passive_mode: bool,
timeout: Duration,
) -> Result<NativeTlsFtpStream, BatchError> {
let plain_stream = NativeTlsFtpStream::connect(format!("{}:{}", host, port)).map_err(|e| {
BatchError::Io(std::io::Error::new(
std::io::ErrorKind::ConnectionRefused,
format!("Failed to connect to FTP server: {}", e),
))
})?;
let tls_connector = TlsConnector::new()
.map_err(|e| BatchError::Configuration(format!("Failed to create TLS connector: {}", e)))?;
let mut ftp_stream = plain_stream
.into_secure(NativeTlsConnector::from(tls_connector), host)
.map_err(|e| {
BatchError::Io(std::io::Error::new(
std::io::ErrorKind::ConnectionRefused,
format!("Failed to establish FTPS connection: {}", e),
))
})?;
ftp_stream
.login(username, password)
.map_err(|e| BatchError::Configuration(format!("FTPS login failed: {}", e)))?;
ftp_stream
.get_ref()
.set_read_timeout(Some(timeout))
.map_err(|e| BatchError::Configuration(format!("Failed to set read timeout: {}", e)))?;
ftp_stream
.get_ref()
.set_write_timeout(Some(timeout))
.map_err(|e| BatchError::Configuration(format!("Failed to set write timeout: {}", e)))?;
let mode = if passive_mode {
Mode::Passive
} else {
Mode::Active
};
ftp_stream.set_mode(mode);
Ok(ftp_stream)
}
#[derive(Debug)]
pub struct FtpPutTasklet {
host: String,
port: u16,
username: String,
password: String,
local_file: PathBuf,
remote_file: String,
passive_mode: bool,
timeout: Duration,
secure: bool,
}
impl FtpPutTasklet {
pub fn new<P: AsRef<Path>>(
host: &str,
port: u16,
username: &str,
password: &str,
local_file: P,
remote_file: &str,
) -> Result<Self, BatchError> {
let local_path = local_file.as_ref().to_path_buf();
if !local_path.exists() {
return Err(BatchError::Configuration(format!(
"Local file does not exist: {}",
local_path.display()
)));
}
Ok(Self {
host: host.to_string(),
port,
username: username.to_string(),
password: password.to_string(),
local_file: local_path,
remote_file: remote_file.to_string(),
passive_mode: true,
timeout: Duration::from_secs(30),
secure: false,
})
}
pub fn set_passive_mode(&mut self, passive: bool) {
self.passive_mode = passive;
}
pub fn set_timeout(&mut self, timeout: Duration) {
self.timeout = timeout;
}
}
impl Tasklet for FtpPutTasklet {
fn execute(&self, _step_execution: &StepExecution) -> Result<RepeatStatus, BatchError> {
let protocol = if self.secure { "FTPS" } else { "FTP" };
info!(
"Starting {} PUT: {} -> {}:{}{}",
protocol,
self.local_file.display(),
self.host,
self.port,
self.remote_file
);
let file = File::open(&self.local_file).map_err(BatchError::Io)?;
let mut reader = BufReader::new(file);
if self.secure {
#[cfg(feature = "ftp")]
{
let mut ftp_stream = setup_ftps_connection(
&self.host,
self.port,
&self.username,
&self.password,
self.passive_mode,
self.timeout,
)?;
ftp_stream
.put_file(&self.remote_file, &mut reader)
.map_err(|e| {
BatchError::Io(std::io::Error::other(format!("FTPS upload failed: {}", e)))
})?;
let _ = ftp_stream.quit();
}
#[cfg(not(feature = "ftp"))]
{
return Err(BatchError::Configuration(
"FTPS support requires the 'ftp' feature to be enabled".to_string(),
));
}
} else {
let mut ftp_stream = setup_ftp_connection(
&self.host,
self.port,
&self.username,
&self.password,
self.passive_mode,
self.timeout,
)?;
ftp_stream
.put_file(&self.remote_file, &mut reader)
.map_err(|e| {
BatchError::Io(std::io::Error::other(format!("FTP upload failed: {}", e)))
})?;
let _ = ftp_stream.quit();
}
info!(
"{} PUT completed successfully: {} uploaded to {}:{}{}",
protocol,
self.local_file.display(),
self.host,
self.port,
self.remote_file
);
Ok(RepeatStatus::Finished)
}
}
#[derive(Debug)]
pub struct FtpGetTasklet {
host: String,
port: u16,
username: String,
password: String,
remote_file: String,
local_file: PathBuf,
passive_mode: bool,
timeout: Duration,
secure: bool,
}
impl FtpGetTasklet {
pub fn new<P: AsRef<Path>>(
host: &str,
port: u16,
username: &str,
password: &str,
remote_file: &str,
local_file: P,
) -> Result<Self, BatchError> {
let local_path = local_file.as_ref().to_path_buf();
if let Some(parent) = local_path.parent()
&& !parent.exists()
{
std::fs::create_dir_all(parent).map_err(BatchError::Io)?;
}
Ok(Self {
host: host.to_string(),
port,
username: username.to_string(),
password: password.to_string(),
remote_file: remote_file.to_string(),
local_file: local_path,
passive_mode: true,
timeout: Duration::from_secs(30),
secure: false,
})
}
pub fn set_passive_mode(&mut self, passive: bool) {
self.passive_mode = passive;
}
pub fn set_timeout(&mut self, timeout: Duration) {
self.timeout = timeout;
}
}
impl Tasklet for FtpGetTasklet {
fn execute(&self, _step_execution: &StepExecution) -> Result<RepeatStatus, BatchError> {
let protocol = if self.secure { "FTPS" } else { "FTP" };
info!(
"Starting {} GET: {}:{}{} -> {}",
protocol,
self.host,
self.port,
self.remote_file,
self.local_file.display()
);
let local_file_path = self.local_file.clone();
if self.secure {
#[cfg(feature = "ftp")]
{
let mut ftp_stream = setup_ftps_connection(
&self.host,
self.port,
&self.username,
&self.password,
self.passive_mode,
self.timeout,
)?;
ftp_stream
.retr(&self.remote_file, |stream| {
let mut file =
File::create(&local_file_path).map_err(io_error_to_ftp_error)?;
std::io::copy(stream, &mut file).map_err(io_error_to_ftp_error)?;
file.sync_all().map_err(io_error_to_ftp_error)?;
Ok(())
})
.map_err(|e| {
BatchError::Io(std::io::Error::other(format!(
"FTPS streaming download failed: {}",
e
)))
})?;
let _ = ftp_stream.quit();
}
#[cfg(not(feature = "ftp"))]
{
return Err(BatchError::Configuration(
"FTPS support requires the 'ftp' feature to be enabled".to_string(),
));
}
} else {
let mut ftp_stream = setup_ftp_connection(
&self.host,
self.port,
&self.username,
&self.password,
self.passive_mode,
self.timeout,
)?;
ftp_stream
.retr(&self.remote_file, |stream| {
let mut file = File::create(&local_file_path).map_err(io_error_to_ftp_error)?;
std::io::copy(stream, &mut file).map_err(io_error_to_ftp_error)?;
file.sync_all().map_err(io_error_to_ftp_error)?;
Ok(())
})
.map_err(|e| {
BatchError::Io(std::io::Error::other(format!(
"FTP streaming download failed: {}",
e
)))
})?;
let _ = ftp_stream.quit();
}
info!(
"{} GET completed successfully: {}:{}{} downloaded to {}",
protocol,
self.host,
self.port,
self.remote_file,
self.local_file.display()
);
Ok(RepeatStatus::Finished)
}
}
#[derive(Debug)]
pub struct FtpPutFolderTasklet {
host: String,
port: u16,
username: String,
password: String,
local_folder: PathBuf,
remote_folder: String,
passive_mode: bool,
timeout: Duration,
create_directories: bool,
recursive: bool,
secure: bool,
}
impl FtpPutFolderTasklet {
pub fn new<P: AsRef<Path>>(
host: &str,
port: u16,
username: &str,
password: &str,
local_folder: P,
remote_folder: &str,
) -> Result<Self, BatchError> {
let local_path = local_folder.as_ref().to_path_buf();
if !local_path.exists() {
return Err(BatchError::Configuration(format!(
"Local folder does not exist: {}",
local_path.display()
)));
}
if !local_path.is_dir() {
return Err(BatchError::Configuration(format!(
"Local path is not a directory: {}",
local_path.display()
)));
}
Ok(Self {
host: host.to_string(),
port,
username: username.to_string(),
password: password.to_string(),
local_folder: local_path,
remote_folder: remote_folder.to_string(),
passive_mode: true,
timeout: Duration::from_secs(30),
create_directories: true,
recursive: false,
secure: false,
})
}
pub fn set_passive_mode(&mut self, passive: bool) {
self.passive_mode = passive;
}
pub fn set_timeout(&mut self, timeout: Duration) {
self.timeout = timeout;
}
pub fn set_create_directories(&mut self, create: bool) {
self.create_directories = create;
}
pub fn set_recursive(&mut self, recursive: bool) {
self.recursive = recursive;
}
fn upload_directory(
&self,
ftp_stream: &mut FtpStream,
local_dir: &Path,
remote_dir: &str,
) -> Result<(), BatchError> {
let entries = fs::read_dir(local_dir).map_err(BatchError::Io)?;
for entry in entries {
let entry = entry.map_err(BatchError::Io)?;
let local_path = entry.path();
let file_name = entry.file_name();
let file_name_str = file_name.to_string_lossy();
let remote_path = if remote_dir.is_empty() {
file_name_str.to_string()
} else {
format!("{}/{}", remote_dir, file_name_str)
};
if local_path.is_file() {
info!(
"Uploading file: {} -> {}",
local_path.display(),
remote_path
);
let file = File::open(&local_path).map_err(BatchError::Io)?;
let mut reader = BufReader::new(file);
ftp_stream
.put_file(&remote_path, &mut reader)
.map_err(|e| {
BatchError::Io(std::io::Error::other(format!(
"FTP upload failed for {}: {}",
local_path.display(),
e
)))
})?;
} else if local_path.is_dir() && self.recursive {
info!("Creating remote directory: {}", remote_path);
if self.create_directories {
let _ = ftp_stream.mkdir(&remote_path);
}
self.upload_directory(ftp_stream, &local_path, &remote_path)?;
}
}
Ok(())
}
#[cfg(feature = "ftp")]
fn upload_directory_secure(
&self,
ftp_stream: &mut NativeTlsFtpStream,
local_dir: &Path,
remote_dir: &str,
) -> Result<(), BatchError> {
let entries = fs::read_dir(local_dir).map_err(BatchError::Io)?;
for entry in entries {
let entry = entry.map_err(BatchError::Io)?;
let local_path = entry.path();
let file_name = entry.file_name();
let file_name_str = file_name.to_string_lossy();
let remote_path = if remote_dir.is_empty() {
file_name_str.to_string()
} else {
format!("{}/{}", remote_dir, file_name_str)
};
if local_path.is_file() {
info!(
"Uploading file (FTPS): {} -> {}",
local_path.display(),
remote_path
);
let file = File::open(&local_path).map_err(BatchError::Io)?;
let mut reader = BufReader::new(file);
ftp_stream
.put_file(&remote_path, &mut reader)
.map_err(|e| {
BatchError::Io(std::io::Error::other(format!(
"FTPS upload failed for {}: {}",
local_path.display(),
e
)))
})?;
} else if local_path.is_dir() && self.recursive {
info!("Creating remote directory (FTPS): {}", remote_path);
if self.create_directories {
let _ = ftp_stream.mkdir(&remote_path);
}
self.upload_directory_secure(ftp_stream, &local_path, &remote_path)?;
}
}
Ok(())
}
}
impl Tasklet for FtpPutFolderTasklet {
fn execute(&self, _step_execution: &StepExecution) -> Result<RepeatStatus, BatchError> {
let protocol = if self.secure { "FTPS" } else { "FTP" };
info!(
"Starting {} PUT FOLDER: {} -> {}:{}{}",
protocol,
self.local_folder.display(),
self.host,
self.port,
self.remote_folder
);
if self.secure {
#[cfg(feature = "ftp")]
{
let mut ftp_stream = setup_ftps_connection(
&self.host,
self.port,
&self.username,
&self.password,
self.passive_mode,
self.timeout,
)?;
if self.create_directories && !self.remote_folder.is_empty() {
let _ = ftp_stream.mkdir(&self.remote_folder);
}
self.upload_directory_secure(
&mut ftp_stream,
&self.local_folder,
&self.remote_folder,
)?;
let _ = ftp_stream.quit();
}
#[cfg(not(feature = "ftp"))]
{
return Err(BatchError::Configuration(
"FTPS support requires the 'ftp' feature to be enabled".to_string(),
));
}
} else {
let mut ftp_stream = setup_ftp_connection(
&self.host,
self.port,
&self.username,
&self.password,
self.passive_mode,
self.timeout,
)?;
if self.create_directories && !self.remote_folder.is_empty() {
let _ = ftp_stream.mkdir(&self.remote_folder);
}
self.upload_directory(&mut ftp_stream, &self.local_folder, &self.remote_folder)?;
let _ = ftp_stream.quit();
}
info!(
"{} PUT FOLDER completed successfully: {} uploaded to {}:{}{}",
protocol,
self.local_folder.display(),
self.host,
self.port,
self.remote_folder
);
Ok(RepeatStatus::Finished)
}
}
#[derive(Debug)]
pub struct FtpGetFolderTasklet {
host: String,
port: u16,
username: String,
password: String,
remote_folder: String,
local_folder: PathBuf,
passive_mode: bool,
timeout: Duration,
create_directories: bool,
recursive: bool,
secure: bool,
}
impl FtpGetFolderTasklet {
pub fn new<P: AsRef<Path>>(
host: &str,
port: u16,
username: &str,
password: &str,
remote_folder: &str,
local_folder: P,
) -> Result<Self, BatchError> {
let local_path = local_folder.as_ref().to_path_buf();
Ok(Self {
host: host.to_string(),
port,
username: username.to_string(),
password: password.to_string(),
remote_folder: remote_folder.to_string(),
local_folder: local_path,
passive_mode: true,
timeout: Duration::from_secs(30),
create_directories: true,
recursive: false,
secure: false,
})
}
pub fn set_passive_mode(&mut self, passive: bool) {
self.passive_mode = passive;
}
pub fn set_timeout(&mut self, timeout: Duration) {
self.timeout = timeout;
}
pub fn set_create_directories(&mut self, create: bool) {
self.create_directories = create;
}
pub fn set_recursive(&mut self, recursive: bool) {
self.recursive = recursive;
}
fn download_directory(
&self,
ftp_stream: &mut FtpStream,
remote_dir: &str,
local_dir: &Path,
) -> Result<(), BatchError> {
let files = ftp_stream.nlst(Some(remote_dir)).map_err(|e| {
BatchError::Io(std::io::Error::other(format!(
"Failed to list remote directory {}: {}",
remote_dir, e
)))
})?;
for file_path in files {
let file_name = Path::new(&file_path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(&file_path);
let local_path = local_dir.join(file_name);
let remote_full_path = if remote_dir.is_empty() {
file_path.clone()
} else {
format!("{}/{}", remote_dir, file_name)
};
let download_result = {
let local_path_clone = local_path.clone();
ftp_stream.retr(&remote_full_path, |stream| {
let mut file =
File::create(&local_path_clone).map_err(io_error_to_ftp_error)?;
std::io::copy(stream, &mut file).map_err(io_error_to_ftp_error)?;
file.sync_all().map_err(io_error_to_ftp_error)?;
Ok(())
})
};
match download_result {
Ok(_) => {
info!(
"Streaming downloaded file: {} -> {}",
remote_full_path,
local_path.display()
);
if self.create_directories
&& let Some(parent) = local_path.parent()
{
fs::create_dir_all(parent).map_err(BatchError::Io)?;
}
}
Err(_) if self.recursive => {
info!("Attempting to download directory: {}", remote_full_path);
if self.create_directories {
fs::create_dir_all(&local_path).map_err(BatchError::Io)?;
}
if let Err(e) =
self.download_directory(ftp_stream, &remote_full_path, &local_path)
{
info!(
"Failed to download as directory, skipping: {} ({})",
remote_full_path, e
);
}
}
Err(e) => {
info!(
"Skipping item that couldn't be downloaded: {} ({})",
remote_full_path, e
);
}
}
}
Ok(())
}
#[cfg(feature = "ftp")]
fn download_directory_secure(
&self,
ftp_stream: &mut NativeTlsFtpStream,
remote_dir: &str,
local_dir: &Path,
) -> Result<(), BatchError> {
let files = ftp_stream.nlst(Some(remote_dir)).map_err(|e| {
BatchError::Io(std::io::Error::other(format!(
"Failed to list remote directory {}: {}",
remote_dir, e
)))
})?;
for file_path in files {
let file_name = Path::new(&file_path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(&file_path);
let local_path = local_dir.join(file_name);
let remote_full_path = if remote_dir.is_empty() {
file_path.clone()
} else {
format!("{}/{}", remote_dir, file_name)
};
let download_result = {
let local_path_clone = local_path.clone();
ftp_stream.retr(&remote_full_path, |stream| {
let mut file =
File::create(&local_path_clone).map_err(io_error_to_ftp_error)?;
std::io::copy(stream, &mut file).map_err(io_error_to_ftp_error)?;
file.sync_all().map_err(io_error_to_ftp_error)?;
Ok(())
})
};
match download_result {
Ok(_) => {
info!(
"Streaming downloaded file (FTPS): {} -> {}",
remote_full_path,
local_path.display()
);
if self.create_directories
&& let Some(parent) = local_path.parent()
{
fs::create_dir_all(parent).map_err(BatchError::Io)?;
}
}
Err(_) if self.recursive => {
info!(
"Attempting to download directory (FTPS): {}",
remote_full_path
);
if self.create_directories {
fs::create_dir_all(&local_path).map_err(BatchError::Io)?;
}
if let Err(e) =
self.download_directory_secure(ftp_stream, &remote_full_path, &local_path)
{
info!(
"Failed to download as directory, skipping: {} ({})",
remote_full_path, e
);
}
}
Err(e) => {
info!(
"Skipping item that couldn't be downloaded (FTPS): {} ({})",
remote_full_path, e
);
}
}
}
Ok(())
}
}
impl Tasklet for FtpGetFolderTasklet {
fn execute(&self, _step_execution: &StepExecution) -> Result<RepeatStatus, BatchError> {
let protocol = if self.secure { "FTPS" } else { "FTP" };
info!(
"Starting {} GET FOLDER: {}:{}{} -> {}",
protocol,
self.host,
self.port,
self.remote_folder,
self.local_folder.display()
);
if self.create_directories {
fs::create_dir_all(&self.local_folder).map_err(BatchError::Io)?;
}
if self.secure {
#[cfg(feature = "ftp")]
{
let mut ftp_stream = setup_ftps_connection(
&self.host,
self.port,
&self.username,
&self.password,
self.passive_mode,
self.timeout,
)?;
self.download_directory_secure(
&mut ftp_stream,
&self.remote_folder,
&self.local_folder,
)?;
let _ = ftp_stream.quit();
}
#[cfg(not(feature = "ftp"))]
{
return Err(BatchError::Configuration(
"FTPS support requires the 'ftp' feature to be enabled".to_string(),
));
}
} else {
let mut ftp_stream = setup_ftp_connection(
&self.host,
self.port,
&self.username,
&self.password,
self.passive_mode,
self.timeout,
)?;
self.download_directory(&mut ftp_stream, &self.remote_folder, &self.local_folder)?;
let _ = ftp_stream.quit();
}
info!(
"{} GET FOLDER completed successfully: {}:{}{} downloaded to {}",
protocol,
self.host,
self.port,
self.remote_folder,
self.local_folder.display()
);
Ok(RepeatStatus::Finished)
}
}
pub struct FtpPutTaskletBuilder {
host: Option<String>,
port: u16,
username: Option<String>,
password: Option<String>,
local_file: Option<PathBuf>,
remote_file: Option<String>,
passive_mode: bool,
timeout: Duration,
secure: bool,
}
impl Default for FtpPutTaskletBuilder {
fn default() -> Self {
Self::new()
}
}
impl FtpPutTaskletBuilder {
pub fn new() -> Self {
Self {
host: None,
port: 21,
username: None,
password: None,
local_file: None,
remote_file: None,
passive_mode: true,
timeout: Duration::from_secs(30),
secure: false,
}
}
pub fn host<S: Into<String>>(mut self, host: S) -> Self {
self.host = Some(host.into());
self
}
pub fn port(mut self, port: u16) -> Self {
self.port = port;
self
}
pub fn username<S: Into<String>>(mut self, username: S) -> Self {
self.username = Some(username.into());
self
}
pub fn password<S: Into<String>>(mut self, password: S) -> Self {
self.password = Some(password.into());
self
}
pub fn local_file<P: AsRef<Path>>(mut self, path: P) -> Self {
self.local_file = Some(path.as_ref().to_path_buf());
self
}
pub fn remote_file<S: Into<String>>(mut self, path: S) -> Self {
self.remote_file = Some(path.into());
self
}
pub fn passive_mode(mut self, passive: bool) -> Self {
self.passive_mode = passive;
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn secure(mut self, secure: bool) -> Self {
self.secure = secure;
self
}
pub fn build(self) -> Result<FtpPutTasklet, BatchError> {
let host = self
.host
.ok_or_else(|| BatchError::Configuration("FTP host is required".to_string()))?;
let username = self
.username
.ok_or_else(|| BatchError::Configuration("FTP username is required".to_string()))?;
let password = self
.password
.ok_or_else(|| BatchError::Configuration("FTP password is required".to_string()))?;
let local_file = self
.local_file
.ok_or_else(|| BatchError::Configuration("Local file path is required".to_string()))?;
let remote_file = self
.remote_file
.ok_or_else(|| BatchError::Configuration("Remote file path is required".to_string()))?;
let mut tasklet = FtpPutTasklet::new(
&host,
self.port,
&username,
&password,
&local_file,
&remote_file,
)?;
tasklet.set_passive_mode(self.passive_mode);
tasklet.set_timeout(self.timeout);
tasklet.secure = self.secure;
Ok(tasklet)
}
}
pub struct FtpGetTaskletBuilder {
host: Option<String>,
port: u16,
username: Option<String>,
password: Option<String>,
remote_file: Option<String>,
local_file: Option<PathBuf>,
passive_mode: bool,
timeout: Duration,
secure: bool,
}
impl Default for FtpGetTaskletBuilder {
fn default() -> Self {
Self::new()
}
}
impl FtpGetTaskletBuilder {
pub fn new() -> Self {
Self {
host: None,
port: 21,
username: None,
password: None,
remote_file: None,
local_file: None,
passive_mode: true,
timeout: Duration::from_secs(30),
secure: false,
}
}
pub fn host<S: Into<String>>(mut self, host: S) -> Self {
self.host = Some(host.into());
self
}
pub fn port(mut self, port: u16) -> Self {
self.port = port;
self
}
pub fn username<S: Into<String>>(mut self, username: S) -> Self {
self.username = Some(username.into());
self
}
pub fn password<S: Into<String>>(mut self, password: S) -> Self {
self.password = Some(password.into());
self
}
pub fn remote_file<S: Into<String>>(mut self, path: S) -> Self {
self.remote_file = Some(path.into());
self
}
pub fn local_file<P: AsRef<Path>>(mut self, path: P) -> Self {
self.local_file = Some(path.as_ref().to_path_buf());
self
}
pub fn passive_mode(mut self, passive: bool) -> Self {
self.passive_mode = passive;
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn secure(mut self, secure: bool) -> Self {
self.secure = secure;
self
}
pub fn build(self) -> Result<FtpGetTasklet, BatchError> {
let host = self
.host
.ok_or_else(|| BatchError::Configuration("FTP host is required".to_string()))?;
let username = self
.username
.ok_or_else(|| BatchError::Configuration("FTP username is required".to_string()))?;
let password = self
.password
.ok_or_else(|| BatchError::Configuration("FTP password is required".to_string()))?;
let remote_file = self
.remote_file
.ok_or_else(|| BatchError::Configuration("Remote file path is required".to_string()))?;
let local_file = self
.local_file
.ok_or_else(|| BatchError::Configuration("Local file path is required".to_string()))?;
let mut tasklet = FtpGetTasklet::new(
&host,
self.port,
&username,
&password,
&remote_file,
&local_file,
)?;
tasklet.set_passive_mode(self.passive_mode);
tasklet.set_timeout(self.timeout);
tasklet.secure = self.secure;
Ok(tasklet)
}
}
pub struct FtpPutFolderTaskletBuilder {
host: Option<String>,
port: u16,
username: Option<String>,
password: Option<String>,
local_folder: Option<PathBuf>,
remote_folder: Option<String>,
passive_mode: bool,
timeout: Duration,
create_directories: bool,
recursive: bool,
secure: bool,
}
impl Default for FtpPutFolderTaskletBuilder {
fn default() -> Self {
Self::new()
}
}
impl FtpPutFolderTaskletBuilder {
pub fn new() -> Self {
Self {
host: None,
port: 21,
username: None,
password: None,
local_folder: None,
remote_folder: None,
passive_mode: true,
timeout: Duration::from_secs(30),
create_directories: true,
recursive: false,
secure: false,
}
}
pub fn host<S: Into<String>>(mut self, host: S) -> Self {
self.host = Some(host.into());
self
}
pub fn port(mut self, port: u16) -> Self {
self.port = port;
self
}
pub fn username<S: Into<String>>(mut self, username: S) -> Self {
self.username = Some(username.into());
self
}
pub fn password<S: Into<String>>(mut self, password: S) -> Self {
self.password = Some(password.into());
self
}
pub fn local_folder<P: AsRef<Path>>(mut self, path: P) -> Self {
self.local_folder = Some(path.as_ref().to_path_buf());
self
}
pub fn remote_folder<S: Into<String>>(mut self, path: S) -> Self {
self.remote_folder = Some(path.into());
self
}
pub fn passive_mode(mut self, passive: bool) -> Self {
self.passive_mode = passive;
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn create_directories(mut self, create: bool) -> Self {
self.create_directories = create;
self
}
pub fn recursive(mut self, recursive: bool) -> Self {
self.recursive = recursive;
self
}
pub fn secure(mut self, secure: bool) -> Self {
self.secure = secure;
self
}
pub fn build(self) -> Result<FtpPutFolderTasklet, BatchError> {
let host = self
.host
.ok_or_else(|| BatchError::Configuration("FTP host is required".to_string()))?;
let username = self
.username
.ok_or_else(|| BatchError::Configuration("FTP username is required".to_string()))?;
let password = self
.password
.ok_or_else(|| BatchError::Configuration("FTP password is required".to_string()))?;
let local_folder = self.local_folder.ok_or_else(|| {
BatchError::Configuration("Local folder path is required".to_string())
})?;
let remote_folder = self.remote_folder.ok_or_else(|| {
BatchError::Configuration("Remote folder path is required".to_string())
})?;
let mut tasklet = FtpPutFolderTasklet::new(
&host,
self.port,
&username,
&password,
&local_folder,
&remote_folder,
)?;
tasklet.set_passive_mode(self.passive_mode);
tasklet.set_timeout(self.timeout);
tasklet.set_create_directories(self.create_directories);
tasklet.set_recursive(self.recursive);
tasklet.secure = self.secure;
Ok(tasklet)
}
}
pub struct FtpGetFolderTaskletBuilder {
host: Option<String>,
port: u16,
username: Option<String>,
password: Option<String>,
remote_folder: Option<String>,
local_folder: Option<PathBuf>,
passive_mode: bool,
timeout: Duration,
create_directories: bool,
recursive: bool,
secure: bool,
}
impl Default for FtpGetFolderTaskletBuilder {
fn default() -> Self {
Self::new()
}
}
impl FtpGetFolderTaskletBuilder {
pub fn new() -> Self {
Self {
host: None,
port: 21,
username: None,
password: None,
remote_folder: None,
local_folder: None,
passive_mode: true,
timeout: Duration::from_secs(30),
create_directories: true,
recursive: false,
secure: false,
}
}
pub fn host<S: Into<String>>(mut self, host: S) -> Self {
self.host = Some(host.into());
self
}
pub fn port(mut self, port: u16) -> Self {
self.port = port;
self
}
pub fn username<S: Into<String>>(mut self, username: S) -> Self {
self.username = Some(username.into());
self
}
pub fn password<S: Into<String>>(mut self, password: S) -> Self {
self.password = Some(password.into());
self
}
pub fn remote_folder<S: Into<String>>(mut self, path: S) -> Self {
self.remote_folder = Some(path.into());
self
}
pub fn local_folder<P: AsRef<Path>>(mut self, path: P) -> Self {
self.local_folder = Some(path.as_ref().to_path_buf());
self
}
pub fn passive_mode(mut self, passive: bool) -> Self {
self.passive_mode = passive;
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn create_directories(mut self, create: bool) -> Self {
self.create_directories = create;
self
}
pub fn recursive(mut self, recursive: bool) -> Self {
self.recursive = recursive;
self
}
pub fn secure(mut self, secure: bool) -> Self {
self.secure = secure;
self
}
pub fn build(self) -> Result<FtpGetFolderTasklet, BatchError> {
let host = self
.host
.ok_or_else(|| BatchError::Configuration("FTP host is required".to_string()))?;
let username = self
.username
.ok_or_else(|| BatchError::Configuration("FTP username is required".to_string()))?;
let password = self
.password
.ok_or_else(|| BatchError::Configuration("FTP password is required".to_string()))?;
let remote_folder = self.remote_folder.ok_or_else(|| {
BatchError::Configuration("Remote folder path is required".to_string())
})?;
let local_folder = self.local_folder.ok_or_else(|| {
BatchError::Configuration("Local folder path is required".to_string())
})?;
let mut tasklet = FtpGetFolderTasklet::new(
&host,
self.port,
&username,
&password,
&remote_folder,
&local_folder,
)?;
tasklet.set_passive_mode(self.passive_mode);
tasklet.set_timeout(self.timeout);
tasklet.set_create_directories(self.create_directories);
tasklet.set_recursive(self.recursive);
tasklet.secure = self.secure;
Ok(tasklet)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::step::StepExecution;
use std::env::temp_dir;
use std::fs;
#[test]
fn should_convert_io_error_to_ftp_error() {
use suppaftp::FtpError;
let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
let ftp_err = io_error_to_ftp_error(io_err);
assert!(matches!(ftp_err, FtpError::ConnectionError(_)));
}
#[test]
fn test_ftp_put_tasklet_creation() -> Result<(), BatchError> {
let temp_dir = temp_dir();
let test_file = temp_dir.join("test_upload.txt");
fs::write(&test_file, "test content").unwrap();
let tasklet = FtpPutTasklet::new(
"localhost",
21,
"testuser",
"testpass",
&test_file,
"/remote/test.txt",
)?;
assert_eq!(tasklet.host, "localhost");
assert_eq!(tasklet.port, 21);
assert_eq!(tasklet.username, "testuser");
assert_eq!(tasklet.remote_file, "/remote/test.txt");
assert!(tasklet.passive_mode);
fs::remove_file(&test_file).ok();
Ok(())
}
#[test]
fn test_ftp_get_tasklet_creation() -> Result<(), BatchError> {
let temp_dir = temp_dir();
let local_file = temp_dir.join("downloaded.txt");
let tasklet = FtpGetTasklet::new(
"localhost",
21,
"testuser",
"testpass",
"/remote/test.txt",
&local_file,
)?;
assert_eq!(tasklet.host, "localhost");
assert_eq!(tasklet.port, 21);
assert_eq!(tasklet.username, "testuser");
assert_eq!(tasklet.remote_file, "/remote/test.txt");
assert!(tasklet.passive_mode);
Ok(())
}
#[test]
fn test_ftp_put_builder() -> Result<(), BatchError> {
let temp_dir = temp_dir();
let test_file = temp_dir.join("test_builder.txt");
fs::write(&test_file, "test content").unwrap();
let tasklet = FtpPutTaskletBuilder::new()
.host("ftp.example.com")
.port(2121)
.username("user")
.password("pass")
.local_file(&test_file)
.remote_file("/upload/file.txt")
.passive_mode(false)
.timeout(Duration::from_secs(60))
.build()?;
assert_eq!(tasklet.host, "ftp.example.com");
assert_eq!(tasklet.port, 2121);
assert!(!tasklet.passive_mode);
assert_eq!(tasklet.timeout, Duration::from_secs(60));
fs::remove_file(&test_file).ok();
Ok(())
}
#[test]
fn test_ftp_get_builder() -> Result<(), BatchError> {
let temp_dir = temp_dir();
let local_file = temp_dir.join("download_builder.txt");
let tasklet = FtpGetTaskletBuilder::new()
.host("ftp.example.com")
.port(2121)
.username("user")
.password("pass")
.remote_file("/download/file.txt")
.local_file(&local_file)
.passive_mode(false)
.timeout(Duration::from_secs(60))
.build()?;
assert_eq!(tasklet.host, "ftp.example.com");
assert_eq!(tasklet.port, 2121);
assert!(!tasklet.passive_mode);
assert_eq!(tasklet.timeout, Duration::from_secs(60));
Ok(())
}
#[test]
fn test_builder_validation() {
let result = FtpPutTaskletBuilder::new()
.username("user")
.password("pass")
.build();
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("FTP host is required")
);
let result = FtpGetTaskletBuilder::new()
.host("localhost")
.password("pass")
.build();
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("FTP username is required")
);
let result = FtpPutTaskletBuilder::new()
.host("localhost")
.username("user")
.build();
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("FTP password is required")
);
let result = FtpPutTaskletBuilder::new()
.host("localhost")
.username("user")
.password("pass")
.remote_file("/remote/file.txt")
.build();
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Local file path is required")
);
let result = FtpGetTaskletBuilder::new()
.host("localhost")
.username("user")
.password("pass")
.local_file("/local/file.txt")
.build();
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Remote file path is required")
);
}
#[test]
fn test_nonexistent_local_file() {
let result = FtpPutTasklet::new(
"localhost",
21,
"user",
"pass",
"/nonexistent/file.txt",
"/remote/file.txt",
);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Local file does not exist")
);
}
#[test]
fn test_ftp_put_tasklet_configuration() -> Result<(), BatchError> {
let temp_dir = temp_dir();
let test_file = temp_dir.join("config_test.txt");
fs::write(&test_file, "test content").unwrap();
let mut tasklet = FtpPutTasklet::new(
"localhost",
21,
"user",
"pass",
&test_file,
"/remote/file.txt",
)?;
assert!(tasklet.passive_mode);
assert_eq!(tasklet.timeout, Duration::from_secs(30));
tasklet.set_passive_mode(false);
tasklet.set_timeout(Duration::from_secs(60));
assert!(!tasklet.passive_mode);
assert_eq!(tasklet.timeout, Duration::from_secs(60));
fs::remove_file(&test_file).ok();
Ok(())
}
#[test]
fn test_ftp_get_tasklet_configuration() -> Result<(), BatchError> {
let temp_dir = temp_dir();
let local_file = temp_dir.join("config_test.txt");
let mut tasklet = FtpGetTasklet::new(
"localhost",
21,
"user",
"pass",
"/remote/file.txt",
&local_file,
)?;
assert!(tasklet.passive_mode);
assert_eq!(tasklet.timeout, Duration::from_secs(30));
tasklet.set_passive_mode(false);
tasklet.set_timeout(Duration::from_secs(120));
assert!(!tasklet.passive_mode);
assert_eq!(tasklet.timeout, Duration::from_secs(120));
Ok(())
}
#[test]
fn test_ftp_put_tasklet_execution_with_connection_error() {
let temp_dir = temp_dir();
let test_file = temp_dir.join("connection_error_test.txt");
fs::write(&test_file, "test content").unwrap();
let tasklet = FtpPutTasklet::new(
"nonexistent.host.invalid",
21,
"user",
"pass",
&test_file,
"/remote/file.txt",
)
.unwrap();
let step_execution = StepExecution::new("test-step");
let result = tasklet.execute(&step_execution);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(matches!(error, BatchError::Io(_)));
assert!(
error
.to_string()
.contains("Failed to connect to FTP server")
);
fs::remove_file(&test_file).ok();
}
#[test]
fn test_ftp_get_tasklet_execution_with_connection_error() {
let temp_dir = temp_dir();
let local_file = temp_dir.join("connection_error_test.txt");
let tasklet = FtpGetTasklet::new(
"nonexistent.host.invalid",
21,
"user",
"pass",
"/remote/file.txt",
&local_file,
)
.unwrap();
let step_execution = StepExecution::new("test-step");
let result = tasklet.execute(&step_execution);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(matches!(error, BatchError::Io(_)));
assert!(
error
.to_string()
.contains("Failed to connect to FTP server")
);
}
#[test]
fn test_ftp_put_tasklet_secure_execution_with_connection_error() {
let temp_dir = temp_dir();
let test_file = temp_dir.join("secure_conn_error_test.txt");
fs::write(&test_file, "test content").unwrap();
let tasklet = FtpPutTaskletBuilder::new()
.host("nonexistent.host.invalid")
.port(990)
.username("user")
.password("pass")
.local_file(&test_file)
.remote_file("/secure/file.txt")
.secure(true)
.build()
.unwrap();
let step_execution = StepExecution::new("test-step");
let result = tasklet.execute(&step_execution);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), BatchError::Io(_)));
fs::remove_file(&test_file).ok();
}
#[test]
fn test_ftp_get_tasklet_secure_execution_with_connection_error() {
let temp_dir = temp_dir();
let local_file = temp_dir.join("secure_get_conn_error_test.txt");
let tasklet = FtpGetTaskletBuilder::new()
.host("nonexistent.host.invalid")
.port(990)
.username("user")
.password("pass")
.remote_file("/secure/file.txt")
.local_file(&local_file)
.secure(true)
.build()
.unwrap();
let step_execution = StepExecution::new("test-step");
let result = tasklet.execute(&step_execution);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), BatchError::Io(_)));
}
#[test]
fn test_ftp_put_folder_tasklet_creation() -> Result<(), BatchError> {
let temp_dir = temp_dir();
let test_folder = temp_dir.join("test_upload_folder");
fs::create_dir_all(&test_folder).unwrap();
fs::write(test_folder.join("file1.txt"), "content1").unwrap();
fs::write(test_folder.join("file2.txt"), "content2").unwrap();
let tasklet = FtpPutFolderTasklet::new(
"localhost",
21,
"testuser",
"testpass",
&test_folder,
"/remote/folder",
)?;
assert_eq!(tasklet.host, "localhost");
assert_eq!(tasklet.port, 21);
assert_eq!(tasklet.username, "testuser");
assert_eq!(tasklet.remote_folder, "/remote/folder");
assert!(tasklet.passive_mode);
assert!(tasklet.create_directories);
assert!(!tasklet.recursive);
fs::remove_dir_all(&test_folder).ok();
Ok(())
}
#[test]
fn test_ftp_get_folder_tasklet_creation() -> Result<(), BatchError> {
let temp_dir = temp_dir();
let local_folder = temp_dir.join("download_folder");
let tasklet = FtpGetFolderTasklet::new(
"localhost",
21,
"testuser",
"testpass",
"/remote/folder",
&local_folder,
)?;
assert_eq!(tasklet.host, "localhost");
assert_eq!(tasklet.port, 21);
assert_eq!(tasklet.username, "testuser");
assert_eq!(tasklet.remote_folder, "/remote/folder");
assert!(tasklet.passive_mode);
assert!(tasklet.create_directories);
assert!(!tasklet.recursive);
Ok(())
}
#[test]
fn test_ftp_put_folder_builder() -> Result<(), BatchError> {
let temp_dir = temp_dir();
let test_folder = temp_dir.join("test_builder_folder");
fs::create_dir_all(&test_folder).unwrap();
fs::write(test_folder.join("file.txt"), "content").unwrap();
let tasklet = FtpPutFolderTaskletBuilder::new()
.host("ftp.example.com")
.port(2121)
.username("user")
.password("pass")
.local_folder(&test_folder)
.remote_folder("/upload/folder")
.passive_mode(false)
.timeout(Duration::from_secs(60))
.create_directories(false)
.recursive(true)
.build()?;
assert_eq!(tasklet.host, "ftp.example.com");
assert_eq!(tasklet.port, 2121);
assert!(!tasklet.passive_mode);
assert_eq!(tasklet.timeout, Duration::from_secs(60));
assert!(!tasklet.create_directories);
assert!(tasklet.recursive);
fs::remove_dir_all(&test_folder).ok();
Ok(())
}
#[test]
fn test_ftp_get_folder_builder() -> Result<(), BatchError> {
let temp_dir = temp_dir();
let local_folder = temp_dir.join("download_builder_folder");
let tasklet = FtpGetFolderTaskletBuilder::new()
.host("ftp.example.com")
.port(2121)
.username("user")
.password("pass")
.remote_folder("/download/folder")
.local_folder(&local_folder)
.passive_mode(false)
.timeout(Duration::from_secs(60))
.create_directories(false)
.recursive(true)
.build()?;
assert_eq!(tasklet.host, "ftp.example.com");
assert_eq!(tasklet.port, 2121);
assert!(!tasklet.passive_mode);
assert_eq!(tasklet.timeout, Duration::from_secs(60));
assert!(!tasklet.create_directories);
assert!(tasklet.recursive);
Ok(())
}
#[test]
fn test_folder_builder_validation() {
let result = FtpPutFolderTaskletBuilder::new()
.username("user")
.password("pass")
.build();
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("FTP host is required")
);
let result = FtpGetFolderTaskletBuilder::new()
.host("localhost")
.password("pass")
.build();
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("FTP username is required")
);
let result = FtpPutFolderTaskletBuilder::new()
.host("localhost")
.username("user")
.password("pass")
.remote_folder("/remote/folder")
.build();
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Local folder path is required")
);
let result = FtpGetFolderTaskletBuilder::new()
.host("localhost")
.username("user")
.password("pass")
.local_folder("/local/folder")
.build();
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Remote folder path is required")
);
}
#[test]
fn test_nonexistent_local_folder() {
let result = FtpPutFolderTasklet::new(
"localhost",
21,
"user",
"pass",
"/nonexistent/folder",
"/remote/folder",
);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Local folder does not exist")
);
}
#[test]
fn test_local_file_not_directory() {
let temp_dir = temp_dir();
let test_file = temp_dir.join("not_a_directory.txt");
fs::write(&test_file, "content").unwrap();
let result = FtpPutFolderTasklet::new(
"localhost",
21,
"user",
"pass",
&test_file,
"/remote/folder",
);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Local path is not a directory")
);
fs::remove_file(&test_file).ok();
}
#[test]
fn test_ftp_put_folder_tasklet_configuration() -> Result<(), BatchError> {
let temp_dir = temp_dir();
let test_folder = temp_dir.join("config_folder_test");
fs::create_dir_all(&test_folder).unwrap();
fs::write(test_folder.join("file.txt"), "content").unwrap();
let mut tasklet = FtpPutFolderTasklet::new(
"localhost",
21,
"user",
"pass",
&test_folder,
"/remote/folder",
)?;
assert!(tasklet.passive_mode);
assert_eq!(tasklet.timeout, Duration::from_secs(30));
assert!(tasklet.create_directories);
assert!(!tasklet.recursive);
tasklet.set_passive_mode(false);
tasklet.set_timeout(Duration::from_secs(90));
tasklet.set_create_directories(false);
tasklet.set_recursive(true);
assert!(!tasklet.passive_mode);
assert_eq!(tasklet.timeout, Duration::from_secs(90));
assert!(!tasklet.create_directories);
assert!(tasklet.recursive);
fs::remove_dir_all(&test_folder).ok();
Ok(())
}
#[test]
fn test_ftp_get_folder_tasklet_configuration() -> Result<(), BatchError> {
let temp_dir = temp_dir();
let local_folder = temp_dir.join("config_folder_test");
let mut tasklet = FtpGetFolderTasklet::new(
"localhost",
21,
"user",
"pass",
"/remote/folder",
&local_folder,
)?;
assert!(tasklet.passive_mode);
assert_eq!(tasklet.timeout, Duration::from_secs(30));
assert!(tasklet.create_directories);
assert!(!tasklet.recursive);
tasklet.set_passive_mode(false);
tasklet.set_timeout(Duration::from_secs(180));
tasklet.set_create_directories(false);
tasklet.set_recursive(true);
assert!(!tasklet.passive_mode);
assert_eq!(tasklet.timeout, Duration::from_secs(180));
assert!(!tasklet.create_directories);
assert!(tasklet.recursive);
Ok(())
}
#[test]
fn test_ftp_put_folder_tasklet_execution_with_connection_error() {
let temp_dir = temp_dir();
let test_folder = temp_dir.join("connection_error_folder_test");
fs::create_dir_all(&test_folder).unwrap();
fs::write(test_folder.join("file.txt"), "content").unwrap();
let tasklet = FtpPutFolderTasklet::new(
"nonexistent.host.invalid",
21,
"user",
"pass",
&test_folder,
"/remote/folder",
)
.unwrap();
let step_execution = StepExecution::new("test-step");
let result = tasklet.execute(&step_execution);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(matches!(error, BatchError::Io(_)));
assert!(
error
.to_string()
.contains("Failed to connect to FTP server")
);
fs::remove_dir_all(&test_folder).ok();
}
#[test]
fn test_ftp_get_folder_tasklet_execution_with_connection_error() {
let temp_dir = temp_dir();
let local_folder = temp_dir.join("connection_error_folder_test");
let tasklet = FtpGetFolderTasklet::new(
"nonexistent.host.invalid",
21,
"user",
"pass",
"/remote/folder",
&local_folder,
)
.unwrap();
let step_execution = StepExecution::new("test-step");
let result = tasklet.execute(&step_execution);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(matches!(error, BatchError::Io(_)));
assert!(
error
.to_string()
.contains("Failed to connect to FTP server")
);
}
#[test]
fn test_builder_default_implementations() {
let _put_builder = FtpPutTaskletBuilder::default();
let _get_builder = FtpGetTaskletBuilder::default();
let _put_folder_builder = FtpPutFolderTaskletBuilder::default();
let _get_folder_builder = FtpGetFolderTaskletBuilder::default();
}
#[test]
fn test_builder_fluent_interface() -> Result<(), BatchError> {
let temp_dir = temp_dir();
let test_file = temp_dir.join("fluent_test.txt");
fs::write(&test_file, "test content").unwrap();
let tasklet = FtpPutTaskletBuilder::new()
.host("example.com")
.port(2121)
.username("testuser")
.password("testpass")
.local_file(&test_file)
.remote_file("/remote/test.txt")
.passive_mode(true)
.timeout(Duration::from_secs(45))
.build()?;
assert_eq!(tasklet.host, "example.com");
assert_eq!(tasklet.port, 2121);
assert_eq!(tasklet.username, "testuser");
assert_eq!(tasklet.password, "testpass");
assert_eq!(tasklet.remote_file, "/remote/test.txt");
assert!(tasklet.passive_mode);
assert_eq!(tasklet.timeout, Duration::from_secs(45));
fs::remove_file(&test_file).ok();
Ok(())
}
#[test]
fn test_error_message_quality() {
let result = FtpPutTaskletBuilder::new().build();
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("FTP host is required"));
let result = FtpPutTaskletBuilder::new().host("localhost").build();
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("FTP username is required"));
}
#[test]
fn test_path_handling() -> Result<(), BatchError> {
let temp_dir = temp_dir();
let test_file = temp_dir.join("path_test.txt");
fs::write(&test_file, "test content").unwrap();
let tasklet1 = FtpPutTasklet::new(
"localhost",
21,
"user",
"pass",
&test_file,
"/remote/file.txt",
)?;
let tasklet2 = FtpPutTasklet::new(
"localhost",
21,
"user",
"pass",
test_file.as_path(),
"/remote/file.txt",
)?;
assert_eq!(tasklet1.local_file, tasklet2.local_file);
fs::remove_file(&test_file).ok();
Ok(())
}
#[test]
fn test_timeout_configuration() -> Result<(), BatchError> {
let temp_dir = temp_dir();
let test_file = temp_dir.join("timeout_test.txt");
fs::write(&test_file, "test content").unwrap();
let tasklet = FtpPutTaskletBuilder::new()
.host("localhost")
.username("user")
.password("pass")
.local_file(&test_file)
.remote_file("/remote/file.txt")
.timeout(Duration::from_millis(500))
.build()?;
assert_eq!(tasklet.timeout, Duration::from_millis(500));
let tasklet = FtpPutTaskletBuilder::new()
.host("localhost")
.username("user")
.password("pass")
.local_file(&test_file)
.remote_file("/remote/file.txt")
.timeout(Duration::from_secs(300))
.build()?;
assert_eq!(tasklet.timeout, Duration::from_secs(300));
fs::remove_file(&test_file).ok();
Ok(())
}
#[test]
fn test_port_configuration() -> Result<(), BatchError> {
let temp_dir = temp_dir();
let test_file = temp_dir.join("port_test.txt");
fs::write(&test_file, "test content").unwrap();
let tasklet = FtpPutTaskletBuilder::new()
.host("localhost")
.port(990) .username("user")
.password("pass")
.local_file(&test_file)
.remote_file("/remote/file.txt")
.build()?;
assert_eq!(tasklet.port, 990);
let tasklet = FtpPutTaskletBuilder::new()
.host("localhost")
.port(2121) .username("user")
.password("pass")
.local_file(&test_file)
.remote_file("/remote/file.txt")
.build()?;
assert_eq!(tasklet.port, 2121);
fs::remove_file(&test_file).ok();
Ok(())
}
#[test]
fn test_passive_mode_configuration() -> Result<(), BatchError> {
let temp_dir = temp_dir();
let test_file = temp_dir.join("passive_test.txt");
fs::write(&test_file, "test content").unwrap();
let tasklet = FtpPutTaskletBuilder::new()
.host("localhost")
.username("user")
.password("pass")
.local_file(&test_file)
.remote_file("/remote/file.txt")
.passive_mode(true)
.build()?;
assert!(tasklet.passive_mode);
let tasklet = FtpPutTaskletBuilder::new()
.host("localhost")
.username("user")
.password("pass")
.local_file(&test_file)
.remote_file("/remote/file.txt")
.passive_mode(false)
.build()?;
assert!(!tasklet.passive_mode);
fs::remove_file(&test_file).ok();
Ok(())
}
#[test]
fn test_secure_ftp_configuration() -> Result<(), BatchError> {
let temp_dir = temp_dir();
let test_file = temp_dir.join("secure_test.txt");
fs::write(&test_file, "test content").unwrap();
let tasklet = FtpPutTaskletBuilder::new()
.host("localhost")
.username("user")
.password("pass")
.local_file(&test_file)
.remote_file("/remote/file.txt")
.build()?;
assert!(!tasklet.secure);
let tasklet = FtpPutTaskletBuilder::new()
.host("secure-ftp.example.com")
.port(990)
.username("user")
.password("pass")
.local_file(&test_file)
.remote_file("/secure/file.txt")
.secure(true)
.build()?;
assert!(tasklet.secure);
assert_eq!(tasklet.port, 990);
let local_file = temp_dir.join("downloaded_secure.txt");
let get_tasklet = FtpGetTaskletBuilder::new()
.host("secure-ftp.example.com")
.port(990)
.username("user")
.password("pass")
.remote_file("/secure/file.txt")
.local_file(&local_file)
.secure(true)
.build()?;
assert!(get_tasklet.secure);
assert_eq!(get_tasklet.port, 990);
fs::remove_file(&test_file).ok();
Ok(())
}
#[test]
fn test_secure_ftp_folder_configuration() -> Result<(), BatchError> {
let temp_dir = temp_dir();
let test_folder = temp_dir.join("secure_folder_test");
fs::create_dir_all(&test_folder).unwrap();
fs::write(test_folder.join("file.txt"), "test content").unwrap();
let tasklet = FtpPutFolderTaskletBuilder::new()
.host("localhost")
.username("user")
.password("pass")
.local_folder(&test_folder)
.remote_folder("/remote/folder")
.build()?;
assert!(!tasklet.secure);
let tasklet = FtpPutFolderTaskletBuilder::new()
.host("secure-ftp.example.com")
.port(990)
.username("user")
.password("pass")
.local_folder(&test_folder)
.remote_folder("/secure/folder")
.secure(true)
.build()?;
assert!(tasklet.secure);
assert_eq!(tasklet.port, 990);
let local_folder = temp_dir.join("downloaded_secure_folder");
let get_tasklet = FtpGetFolderTaskletBuilder::new()
.host("secure-ftp.example.com")
.port(990)
.username("user")
.password("pass")
.remote_folder("/secure/folder")
.local_folder(&local_folder)
.secure(true)
.build()?;
assert!(get_tasklet.secure);
assert_eq!(get_tasklet.port, 990);
fs::remove_dir_all(&test_folder).ok();
Ok(())
}
}