1use crate::{
113 BatchError,
114 core::step::{RepeatStatus, StepExecution, Tasklet},
115};
116use log::info;
117use std::{
118 fs::{self, File},
119 io::BufReader,
120 path::{Path, PathBuf},
121 time::Duration,
122};
123use suppaftp::{FtpError, FtpStream, Mode};
124
125#[cfg(feature = "ftp")]
126use suppaftp::{NativeTlsConnector, NativeTlsFtpStream};
127
128#[cfg(feature = "ftp")]
129use suppaftp::native_tls::TlsConnector;
130
131fn io_error_to_ftp_error(error: std::io::Error) -> FtpError {
147 FtpError::ConnectionError(error)
148}
149
150fn setup_ftp_connection(
175 host: &str,
176 port: u16,
177 username: &str,
178 password: &str,
179 passive_mode: bool,
180 timeout: Duration,
181) -> Result<FtpStream, BatchError> {
182 let mut ftp_stream = FtpStream::connect(format!("{}:{}", host, port)).map_err(|e| {
184 BatchError::Io(std::io::Error::new(
185 std::io::ErrorKind::ConnectionRefused,
186 format!("Failed to connect to FTP server: {}", e),
187 ))
188 })?;
189
190 ftp_stream
192 .login(username, password)
193 .map_err(|e| BatchError::Configuration(format!("FTP login failed: {}", e)))?;
194
195 ftp_stream
197 .get_ref()
198 .set_read_timeout(Some(timeout))
199 .map_err(|e| BatchError::Configuration(format!("Failed to set read timeout: {}", e)))?;
200 ftp_stream
201 .get_ref()
202 .set_write_timeout(Some(timeout))
203 .map_err(|e| BatchError::Configuration(format!("Failed to set write timeout: {}", e)))?;
204
205 let mode = if passive_mode {
207 Mode::Passive
208 } else {
209 Mode::Active
210 };
211 ftp_stream.set_mode(mode);
212
213 Ok(ftp_stream)
214}
215
216#[cfg(feature = "ftp")]
242fn setup_ftps_connection(
243 host: &str,
244 port: u16,
245 username: &str,
246 password: &str,
247 passive_mode: bool,
248 timeout: Duration,
249) -> Result<NativeTlsFtpStream, BatchError> {
250 let plain_stream = NativeTlsFtpStream::connect(format!("{}:{}", host, port)).map_err(|e| {
252 BatchError::Io(std::io::Error::new(
253 std::io::ErrorKind::ConnectionRefused,
254 format!("Failed to connect to FTP server: {}", e),
255 ))
256 })?;
257
258 let tls_connector = TlsConnector::new()
260 .map_err(|e| BatchError::Configuration(format!("Failed to create TLS connector: {}", e)))?;
261 let mut ftp_stream = plain_stream
262 .into_secure(NativeTlsConnector::from(tls_connector), host)
263 .map_err(|e| {
264 BatchError::Io(std::io::Error::new(
265 std::io::ErrorKind::ConnectionRefused,
266 format!("Failed to establish FTPS connection: {}", e),
267 ))
268 })?;
269
270 ftp_stream
272 .login(username, password)
273 .map_err(|e| BatchError::Configuration(format!("FTPS login failed: {}", e)))?;
274
275 ftp_stream
277 .get_ref()
278 .set_read_timeout(Some(timeout))
279 .map_err(|e| BatchError::Configuration(format!("Failed to set read timeout: {}", e)))?;
280 ftp_stream
281 .get_ref()
282 .set_write_timeout(Some(timeout))
283 .map_err(|e| BatchError::Configuration(format!("Failed to set write timeout: {}", e)))?;
284
285 let mode = if passive_mode {
287 Mode::Passive
288 } else {
289 Mode::Active
290 };
291 ftp_stream.set_mode(mode);
292
293 Ok(ftp_stream)
294}
295
296#[derive(Debug)]
302pub struct FtpPutTasklet {
303 host: String,
305 port: u16,
307 username: String,
309 password: String,
311 local_file: PathBuf,
313 remote_file: String,
315 passive_mode: bool,
317 timeout: Duration,
319 secure: bool,
321}
322
323impl FtpPutTasklet {
324 pub fn new<P: AsRef<Path>>(
326 host: &str,
327 port: u16,
328 username: &str,
329 password: &str,
330 local_file: P,
331 remote_file: &str,
332 ) -> Result<Self, BatchError> {
333 let local_path = local_file.as_ref().to_path_buf();
334
335 if !local_path.exists() {
337 return Err(BatchError::Configuration(format!(
338 "Local file does not exist: {}",
339 local_path.display()
340 )));
341 }
342
343 Ok(Self {
344 host: host.to_string(),
345 port,
346 username: username.to_string(),
347 password: password.to_string(),
348 local_file: local_path,
349 remote_file: remote_file.to_string(),
350 passive_mode: true,
351 timeout: Duration::from_secs(30),
352 secure: false,
353 })
354 }
355
356 pub fn set_passive_mode(&mut self, passive: bool) {
358 self.passive_mode = passive;
359 }
360
361 pub fn set_timeout(&mut self, timeout: Duration) {
363 self.timeout = timeout;
364 }
365}
366
367impl Tasklet for FtpPutTasklet {
368 fn execute(&self, _step_execution: &StepExecution) -> Result<RepeatStatus, BatchError> {
369 let protocol = if self.secure { "FTPS" } else { "FTP" };
370 info!(
371 "Starting {} PUT: {} -> {}:{}{}",
372 protocol,
373 self.local_file.display(),
374 self.host,
375 self.port,
376 self.remote_file
377 );
378
379 let file = File::open(&self.local_file).map_err(BatchError::Io)?;
380 let mut reader = BufReader::new(file);
381
382 if self.secure {
383 #[cfg(feature = "ftp")]
384 {
385 let mut ftp_stream = setup_ftps_connection(
387 &self.host,
388 self.port,
389 &self.username,
390 &self.password,
391 self.passive_mode,
392 self.timeout,
393 )?;
394
395 ftp_stream
397 .put_file(&self.remote_file, &mut reader)
398 .map_err(|e| {
399 BatchError::Io(std::io::Error::other(format!("FTPS upload failed: {}", e)))
400 })?;
401
402 let _ = ftp_stream.quit();
404 }
405 #[cfg(not(feature = "ftp"))]
406 {
407 return Err(BatchError::Configuration(
408 "FTPS support requires the 'ftp' feature to be enabled".to_string(),
409 ));
410 }
411 } else {
412 let mut ftp_stream = setup_ftp_connection(
414 &self.host,
415 self.port,
416 &self.username,
417 &self.password,
418 self.passive_mode,
419 self.timeout,
420 )?;
421
422 ftp_stream
424 .put_file(&self.remote_file, &mut reader)
425 .map_err(|e| {
426 BatchError::Io(std::io::Error::other(format!("FTP upload failed: {}", e)))
427 })?;
428
429 let _ = ftp_stream.quit();
431 }
432
433 info!(
434 "{} PUT completed successfully: {} uploaded to {}:{}{}",
435 protocol,
436 self.local_file.display(),
437 self.host,
438 self.port,
439 self.remote_file
440 );
441
442 Ok(RepeatStatus::Finished)
443 }
444}
445
446#[derive(Debug)]
452pub struct FtpGetTasklet {
453 host: String,
455 port: u16,
457 username: String,
459 password: String,
461 remote_file: String,
463 local_file: PathBuf,
465 passive_mode: bool,
467 timeout: Duration,
469 secure: bool,
471}
472
473impl FtpGetTasklet {
474 pub fn new<P: AsRef<Path>>(
476 host: &str,
477 port: u16,
478 username: &str,
479 password: &str,
480 remote_file: &str,
481 local_file: P,
482 ) -> Result<Self, BatchError> {
483 let local_path = local_file.as_ref().to_path_buf();
484
485 if let Some(parent) = local_path.parent()
487 && !parent.exists()
488 {
489 std::fs::create_dir_all(parent).map_err(BatchError::Io)?;
490 }
491
492 Ok(Self {
493 host: host.to_string(),
494 port,
495 username: username.to_string(),
496 password: password.to_string(),
497 remote_file: remote_file.to_string(),
498 local_file: local_path,
499 passive_mode: true,
500 timeout: Duration::from_secs(30),
501 secure: false,
502 })
503 }
504
505 pub fn set_passive_mode(&mut self, passive: bool) {
507 self.passive_mode = passive;
508 }
509
510 pub fn set_timeout(&mut self, timeout: Duration) {
512 self.timeout = timeout;
513 }
514}
515
516impl Tasklet for FtpGetTasklet {
517 fn execute(&self, _step_execution: &StepExecution) -> Result<RepeatStatus, BatchError> {
518 let protocol = if self.secure { "FTPS" } else { "FTP" };
519 info!(
520 "Starting {} GET: {}:{}{} -> {}",
521 protocol,
522 self.host,
523 self.port,
524 self.remote_file,
525 self.local_file.display()
526 );
527
528 let local_file_path = self.local_file.clone();
529
530 if self.secure {
531 #[cfg(feature = "ftp")]
532 {
533 let mut ftp_stream = setup_ftps_connection(
535 &self.host,
536 self.port,
537 &self.username,
538 &self.password,
539 self.passive_mode,
540 self.timeout,
541 )?;
542
543 ftp_stream
545 .retr(&self.remote_file, |stream| {
546 let mut file =
548 File::create(&local_file_path).map_err(io_error_to_ftp_error)?;
549
550 std::io::copy(stream, &mut file).map_err(io_error_to_ftp_error)?;
552
553 file.sync_all().map_err(io_error_to_ftp_error)?;
555
556 Ok(())
557 })
558 .map_err(|e| {
559 BatchError::Io(std::io::Error::other(format!(
560 "FTPS streaming download failed: {}",
561 e
562 )))
563 })?;
564
565 let _ = ftp_stream.quit();
567 }
568 #[cfg(not(feature = "ftp"))]
569 {
570 return Err(BatchError::Configuration(
571 "FTPS support requires the 'ftp' feature to be enabled".to_string(),
572 ));
573 }
574 } else {
575 let mut ftp_stream = setup_ftp_connection(
577 &self.host,
578 self.port,
579 &self.username,
580 &self.password,
581 self.passive_mode,
582 self.timeout,
583 )?;
584
585 ftp_stream
587 .retr(&self.remote_file, |stream| {
588 let mut file = File::create(&local_file_path).map_err(io_error_to_ftp_error)?;
590
591 std::io::copy(stream, &mut file).map_err(io_error_to_ftp_error)?;
593
594 file.sync_all().map_err(io_error_to_ftp_error)?;
596
597 Ok(())
598 })
599 .map_err(|e| {
600 BatchError::Io(std::io::Error::other(format!(
601 "FTP streaming download failed: {}",
602 e
603 )))
604 })?;
605
606 let _ = ftp_stream.quit();
608 }
609
610 info!(
611 "{} GET completed successfully: {}:{}{} downloaded to {}",
612 protocol,
613 self.host,
614 self.port,
615 self.remote_file,
616 self.local_file.display()
617 );
618
619 Ok(RepeatStatus::Finished)
620 }
621}
622
623#[derive(Debug)]
629pub struct FtpPutFolderTasklet {
630 host: String,
632 port: u16,
634 username: String,
636 password: String,
638 local_folder: PathBuf,
640 remote_folder: String,
642 passive_mode: bool,
644 timeout: Duration,
646 create_directories: bool,
648 recursive: bool,
650 secure: bool,
652}
653
654impl FtpPutFolderTasklet {
655 pub fn new<P: AsRef<Path>>(
657 host: &str,
658 port: u16,
659 username: &str,
660 password: &str,
661 local_folder: P,
662 remote_folder: &str,
663 ) -> Result<Self, BatchError> {
664 let local_path = local_folder.as_ref().to_path_buf();
665
666 if !local_path.exists() {
668 return Err(BatchError::Configuration(format!(
669 "Local folder does not exist: {}",
670 local_path.display()
671 )));
672 }
673
674 if !local_path.is_dir() {
675 return Err(BatchError::Configuration(format!(
676 "Local path is not a directory: {}",
677 local_path.display()
678 )));
679 }
680
681 Ok(Self {
682 host: host.to_string(),
683 port,
684 username: username.to_string(),
685 password: password.to_string(),
686 local_folder: local_path,
687 remote_folder: remote_folder.to_string(),
688 passive_mode: true,
689 timeout: Duration::from_secs(30),
690 create_directories: true,
691 recursive: false,
692 secure: false,
693 })
694 }
695
696 pub fn set_passive_mode(&mut self, passive: bool) {
698 self.passive_mode = passive;
699 }
700
701 pub fn set_timeout(&mut self, timeout: Duration) {
703 self.timeout = timeout;
704 }
705
706 pub fn set_create_directories(&mut self, create: bool) {
708 self.create_directories = create;
709 }
710
711 pub fn set_recursive(&mut self, recursive: bool) {
713 self.recursive = recursive;
714 }
715
716 fn upload_directory(
718 &self,
719 ftp_stream: &mut FtpStream,
720 local_dir: &Path,
721 remote_dir: &str,
722 ) -> Result<(), BatchError> {
723 let entries = fs::read_dir(local_dir).map_err(BatchError::Io)?;
724
725 for entry in entries {
726 let entry = entry.map_err(BatchError::Io)?;
727 let local_path = entry.path();
728 let file_name = entry.file_name();
729 let file_name_str = file_name.to_string_lossy();
730 let remote_path = if remote_dir.is_empty() {
731 file_name_str.to_string()
732 } else {
733 format!("{}/{}", remote_dir, file_name_str)
734 };
735
736 if local_path.is_file() {
737 info!(
738 "Uploading file: {} -> {}",
739 local_path.display(),
740 remote_path
741 );
742
743 let file = File::open(&local_path).map_err(BatchError::Io)?;
744 let mut reader = BufReader::new(file);
745
746 ftp_stream
747 .put_file(&remote_path, &mut reader)
748 .map_err(|e| {
749 BatchError::Io(std::io::Error::other(format!(
750 "FTP upload failed for {}: {}",
751 local_path.display(),
752 e
753 )))
754 })?;
755 } else if local_path.is_dir() && self.recursive {
756 info!("Creating remote directory: {}", remote_path);
757
758 if self.create_directories {
759 let _ = ftp_stream.mkdir(&remote_path);
761 }
762
763 self.upload_directory(ftp_stream, &local_path, &remote_path)?;
765 }
766 }
767
768 Ok(())
769 }
770
771 #[cfg(feature = "ftp")]
773 fn upload_directory_secure(
774 &self,
775 ftp_stream: &mut NativeTlsFtpStream,
776 local_dir: &Path,
777 remote_dir: &str,
778 ) -> Result<(), BatchError> {
779 let entries = fs::read_dir(local_dir).map_err(BatchError::Io)?;
780
781 for entry in entries {
782 let entry = entry.map_err(BatchError::Io)?;
783 let local_path = entry.path();
784 let file_name = entry.file_name();
785 let file_name_str = file_name.to_string_lossy();
786 let remote_path = if remote_dir.is_empty() {
787 file_name_str.to_string()
788 } else {
789 format!("{}/{}", remote_dir, file_name_str)
790 };
791
792 if local_path.is_file() {
793 info!(
794 "Uploading file (FTPS): {} -> {}",
795 local_path.display(),
796 remote_path
797 );
798
799 let file = File::open(&local_path).map_err(BatchError::Io)?;
800 let mut reader = BufReader::new(file);
801
802 ftp_stream
803 .put_file(&remote_path, &mut reader)
804 .map_err(|e| {
805 BatchError::Io(std::io::Error::other(format!(
806 "FTPS upload failed for {}: {}",
807 local_path.display(),
808 e
809 )))
810 })?;
811 } else if local_path.is_dir() && self.recursive {
812 info!("Creating remote directory (FTPS): {}", remote_path);
813
814 if self.create_directories {
815 let _ = ftp_stream.mkdir(&remote_path);
817 }
818
819 self.upload_directory_secure(ftp_stream, &local_path, &remote_path)?;
821 }
822 }
823
824 Ok(())
825 }
826}
827
828impl Tasklet for FtpPutFolderTasklet {
829 fn execute(&self, _step_execution: &StepExecution) -> Result<RepeatStatus, BatchError> {
830 let protocol = if self.secure { "FTPS" } else { "FTP" };
831 info!(
832 "Starting {} PUT FOLDER: {} -> {}:{}{}",
833 protocol,
834 self.local_folder.display(),
835 self.host,
836 self.port,
837 self.remote_folder
838 );
839
840 if self.secure {
841 #[cfg(feature = "ftp")]
842 {
843 let mut ftp_stream = setup_ftps_connection(
845 &self.host,
846 self.port,
847 &self.username,
848 &self.password,
849 self.passive_mode,
850 self.timeout,
851 )?;
852
853 if self.create_directories && !self.remote_folder.is_empty() {
855 let _ = ftp_stream.mkdir(&self.remote_folder);
856 }
857
858 self.upload_directory_secure(
860 &mut ftp_stream,
861 &self.local_folder,
862 &self.remote_folder,
863 )?;
864
865 let _ = ftp_stream.quit();
867 }
868 #[cfg(not(feature = "ftp"))]
869 {
870 return Err(BatchError::Configuration(
871 "FTPS support requires the 'ftp' feature to be enabled".to_string(),
872 ));
873 }
874 } else {
875 let mut ftp_stream = setup_ftp_connection(
877 &self.host,
878 self.port,
879 &self.username,
880 &self.password,
881 self.passive_mode,
882 self.timeout,
883 )?;
884
885 if self.create_directories && !self.remote_folder.is_empty() {
887 let _ = ftp_stream.mkdir(&self.remote_folder);
888 }
889
890 self.upload_directory(&mut ftp_stream, &self.local_folder, &self.remote_folder)?;
892
893 let _ = ftp_stream.quit();
895 }
896
897 info!(
898 "{} PUT FOLDER completed successfully: {} uploaded to {}:{}{}",
899 protocol,
900 self.local_folder.display(),
901 self.host,
902 self.port,
903 self.remote_folder
904 );
905
906 Ok(RepeatStatus::Finished)
907 }
908}
909
910#[derive(Debug)]
916pub struct FtpGetFolderTasklet {
917 host: String,
919 port: u16,
921 username: String,
923 password: String,
925 remote_folder: String,
927 local_folder: PathBuf,
929 passive_mode: bool,
931 timeout: Duration,
933 create_directories: bool,
935 recursive: bool,
937 secure: bool,
939}
940
941impl FtpGetFolderTasklet {
942 pub fn new<P: AsRef<Path>>(
944 host: &str,
945 port: u16,
946 username: &str,
947 password: &str,
948 remote_folder: &str,
949 local_folder: P,
950 ) -> Result<Self, BatchError> {
951 let local_path = local_folder.as_ref().to_path_buf();
952
953 Ok(Self {
954 host: host.to_string(),
955 port,
956 username: username.to_string(),
957 password: password.to_string(),
958 remote_folder: remote_folder.to_string(),
959 local_folder: local_path,
960 passive_mode: true,
961 timeout: Duration::from_secs(30),
962 create_directories: true,
963 recursive: false,
964 secure: false,
965 })
966 }
967
968 pub fn set_passive_mode(&mut self, passive: bool) {
970 self.passive_mode = passive;
971 }
972
973 pub fn set_timeout(&mut self, timeout: Duration) {
975 self.timeout = timeout;
976 }
977
978 pub fn set_create_directories(&mut self, create: bool) {
980 self.create_directories = create;
981 }
982
983 pub fn set_recursive(&mut self, recursive: bool) {
985 self.recursive = recursive;
986 }
987
988 fn download_directory(
990 &self,
991 ftp_stream: &mut FtpStream,
992 remote_dir: &str,
993 local_dir: &Path,
994 ) -> Result<(), BatchError> {
995 let files = ftp_stream.nlst(Some(remote_dir)).map_err(|e| {
997 BatchError::Io(std::io::Error::other(format!(
998 "Failed to list remote directory {}: {}",
999 remote_dir, e
1000 )))
1001 })?;
1002
1003 for file_path in files {
1004 let file_name = Path::new(&file_path)
1005 .file_name()
1006 .and_then(|n| n.to_str())
1007 .unwrap_or(&file_path);
1008
1009 let local_path = local_dir.join(file_name);
1010 let remote_full_path = if remote_dir.is_empty() {
1011 file_path.clone()
1012 } else {
1013 format!("{}/{}", remote_dir, file_name)
1014 };
1015
1016 let download_result = {
1018 let local_path_clone = local_path.clone();
1019 ftp_stream.retr(&remote_full_path, |stream| {
1020 let mut file =
1022 File::create(&local_path_clone).map_err(io_error_to_ftp_error)?;
1023
1024 std::io::copy(stream, &mut file).map_err(io_error_to_ftp_error)?;
1026
1027 file.sync_all().map_err(io_error_to_ftp_error)?;
1029
1030 Ok(())
1031 })
1032 };
1033
1034 match download_result {
1035 Ok(_) => {
1036 info!(
1038 "Streaming downloaded file: {} -> {}",
1039 remote_full_path,
1040 local_path.display()
1041 );
1042
1043 if self.create_directories
1044 && let Some(parent) = local_path.parent()
1045 {
1046 fs::create_dir_all(parent).map_err(BatchError::Io)?;
1047 }
1048 }
1049 Err(_) if self.recursive => {
1050 info!("Attempting to download directory: {}", remote_full_path);
1052
1053 if self.create_directories {
1054 fs::create_dir_all(&local_path).map_err(BatchError::Io)?;
1055 }
1056
1057 if let Err(e) =
1059 self.download_directory(ftp_stream, &remote_full_path, &local_path)
1060 {
1061 info!(
1063 "Failed to download as directory, skipping: {} ({})",
1064 remote_full_path, e
1065 );
1066 }
1067 }
1068 Err(e) => {
1069 info!(
1070 "Skipping item that couldn't be downloaded: {} ({})",
1071 remote_full_path, e
1072 );
1073 }
1074 }
1075 }
1076
1077 Ok(())
1078 }
1079
1080 #[cfg(feature = "ftp")]
1082 fn download_directory_secure(
1083 &self,
1084 ftp_stream: &mut NativeTlsFtpStream,
1085 remote_dir: &str,
1086 local_dir: &Path,
1087 ) -> Result<(), BatchError> {
1088 let files = ftp_stream.nlst(Some(remote_dir)).map_err(|e| {
1090 BatchError::Io(std::io::Error::other(format!(
1091 "Failed to list remote directory {}: {}",
1092 remote_dir, e
1093 )))
1094 })?;
1095
1096 for file_path in files {
1097 let file_name = Path::new(&file_path)
1098 .file_name()
1099 .and_then(|n| n.to_str())
1100 .unwrap_or(&file_path);
1101
1102 let local_path = local_dir.join(file_name);
1103 let remote_full_path = if remote_dir.is_empty() {
1104 file_path.clone()
1105 } else {
1106 format!("{}/{}", remote_dir, file_name)
1107 };
1108
1109 let download_result = {
1111 let local_path_clone = local_path.clone();
1112 ftp_stream.retr(&remote_full_path, |stream| {
1113 let mut file =
1115 File::create(&local_path_clone).map_err(io_error_to_ftp_error)?;
1116
1117 std::io::copy(stream, &mut file).map_err(io_error_to_ftp_error)?;
1119
1120 file.sync_all().map_err(io_error_to_ftp_error)?;
1122
1123 Ok(())
1124 })
1125 };
1126
1127 match download_result {
1128 Ok(_) => {
1129 info!(
1131 "Streaming downloaded file (FTPS): {} -> {}",
1132 remote_full_path,
1133 local_path.display()
1134 );
1135
1136 if self.create_directories
1137 && let Some(parent) = local_path.parent()
1138 {
1139 fs::create_dir_all(parent).map_err(BatchError::Io)?;
1140 }
1141 }
1142 Err(_) if self.recursive => {
1143 info!(
1145 "Attempting to download directory (FTPS): {}",
1146 remote_full_path
1147 );
1148
1149 if self.create_directories {
1150 fs::create_dir_all(&local_path).map_err(BatchError::Io)?;
1151 }
1152
1153 if let Err(e) =
1155 self.download_directory_secure(ftp_stream, &remote_full_path, &local_path)
1156 {
1157 info!(
1159 "Failed to download as directory, skipping: {} ({})",
1160 remote_full_path, e
1161 );
1162 }
1163 }
1164 Err(e) => {
1165 info!(
1166 "Skipping item that couldn't be downloaded (FTPS): {} ({})",
1167 remote_full_path, e
1168 );
1169 }
1170 }
1171 }
1172
1173 Ok(())
1174 }
1175}
1176
1177impl Tasklet for FtpGetFolderTasklet {
1178 fn execute(&self, _step_execution: &StepExecution) -> Result<RepeatStatus, BatchError> {
1179 let protocol = if self.secure { "FTPS" } else { "FTP" };
1180 info!(
1181 "Starting {} GET FOLDER: {}:{}{} -> {}",
1182 protocol,
1183 self.host,
1184 self.port,
1185 self.remote_folder,
1186 self.local_folder.display()
1187 );
1188
1189 if self.create_directories {
1191 fs::create_dir_all(&self.local_folder).map_err(BatchError::Io)?;
1192 }
1193
1194 if self.secure {
1195 #[cfg(feature = "ftp")]
1196 {
1197 let mut ftp_stream = setup_ftps_connection(
1199 &self.host,
1200 self.port,
1201 &self.username,
1202 &self.password,
1203 self.passive_mode,
1204 self.timeout,
1205 )?;
1206
1207 self.download_directory_secure(
1209 &mut ftp_stream,
1210 &self.remote_folder,
1211 &self.local_folder,
1212 )?;
1213
1214 let _ = ftp_stream.quit();
1216 }
1217 #[cfg(not(feature = "ftp"))]
1218 {
1219 return Err(BatchError::Configuration(
1220 "FTPS support requires the 'ftp' feature to be enabled".to_string(),
1221 ));
1222 }
1223 } else {
1224 let mut ftp_stream = setup_ftp_connection(
1226 &self.host,
1227 self.port,
1228 &self.username,
1229 &self.password,
1230 self.passive_mode,
1231 self.timeout,
1232 )?;
1233
1234 self.download_directory(&mut ftp_stream, &self.remote_folder, &self.local_folder)?;
1236
1237 let _ = ftp_stream.quit();
1239 }
1240
1241 info!(
1242 "{} GET FOLDER completed successfully: {}:{}{} downloaded to {}",
1243 protocol,
1244 self.host,
1245 self.port,
1246 self.remote_folder,
1247 self.local_folder.display()
1248 );
1249
1250 Ok(RepeatStatus::Finished)
1251 }
1252}
1253
1254pub struct FtpPutTaskletBuilder {
1256 host: Option<String>,
1257 port: u16,
1258 username: Option<String>,
1259 password: Option<String>,
1260 local_file: Option<PathBuf>,
1261 remote_file: Option<String>,
1262 passive_mode: bool,
1263 timeout: Duration,
1264 secure: bool,
1265}
1266
1267impl Default for FtpPutTaskletBuilder {
1268 fn default() -> Self {
1269 Self::new()
1270 }
1271}
1272
1273impl FtpPutTaskletBuilder {
1274 pub fn new() -> Self {
1276 Self {
1277 host: None,
1278 port: 21,
1279 username: None,
1280 password: None,
1281 local_file: None,
1282 remote_file: None,
1283 passive_mode: true,
1284 timeout: Duration::from_secs(30),
1285 secure: false,
1286 }
1287 }
1288
1289 pub fn host<S: Into<String>>(mut self, host: S) -> Self {
1291 self.host = Some(host.into());
1292 self
1293 }
1294
1295 pub fn port(mut self, port: u16) -> Self {
1297 self.port = port;
1298 self
1299 }
1300
1301 pub fn username<S: Into<String>>(mut self, username: S) -> Self {
1303 self.username = Some(username.into());
1304 self
1305 }
1306
1307 pub fn password<S: Into<String>>(mut self, password: S) -> Self {
1309 self.password = Some(password.into());
1310 self
1311 }
1312
1313 pub fn local_file<P: AsRef<Path>>(mut self, path: P) -> Self {
1315 self.local_file = Some(path.as_ref().to_path_buf());
1316 self
1317 }
1318
1319 pub fn remote_file<S: Into<String>>(mut self, path: S) -> Self {
1321 self.remote_file = Some(path.into());
1322 self
1323 }
1324
1325 pub fn passive_mode(mut self, passive: bool) -> Self {
1327 self.passive_mode = passive;
1328 self
1329 }
1330
1331 pub fn timeout(mut self, timeout: Duration) -> Self {
1333 self.timeout = timeout;
1334 self
1335 }
1336
1337 pub fn secure(mut self, secure: bool) -> Self {
1346 self.secure = secure;
1347 self
1348 }
1349
1350 pub fn build(self) -> Result<FtpPutTasklet, BatchError> {
1352 let host = self
1353 .host
1354 .ok_or_else(|| BatchError::Configuration("FTP host is required".to_string()))?;
1355 let username = self
1356 .username
1357 .ok_or_else(|| BatchError::Configuration("FTP username is required".to_string()))?;
1358 let password = self
1359 .password
1360 .ok_or_else(|| BatchError::Configuration("FTP password is required".to_string()))?;
1361 let local_file = self
1362 .local_file
1363 .ok_or_else(|| BatchError::Configuration("Local file path is required".to_string()))?;
1364 let remote_file = self
1365 .remote_file
1366 .ok_or_else(|| BatchError::Configuration("Remote file path is required".to_string()))?;
1367
1368 let mut tasklet = FtpPutTasklet::new(
1369 &host,
1370 self.port,
1371 &username,
1372 &password,
1373 &local_file,
1374 &remote_file,
1375 )?;
1376
1377 tasklet.set_passive_mode(self.passive_mode);
1378 tasklet.set_timeout(self.timeout);
1379 tasklet.secure = self.secure;
1380
1381 Ok(tasklet)
1382 }
1383}
1384
1385pub struct FtpGetTaskletBuilder {
1387 host: Option<String>,
1388 port: u16,
1389 username: Option<String>,
1390 password: Option<String>,
1391 remote_file: Option<String>,
1392 local_file: Option<PathBuf>,
1393 passive_mode: bool,
1394 timeout: Duration,
1395 secure: bool,
1396}
1397
1398impl Default for FtpGetTaskletBuilder {
1399 fn default() -> Self {
1400 Self::new()
1401 }
1402}
1403
1404impl FtpGetTaskletBuilder {
1405 pub fn new() -> Self {
1407 Self {
1408 host: None,
1409 port: 21,
1410 username: None,
1411 password: None,
1412 remote_file: None,
1413 local_file: None,
1414 passive_mode: true,
1415 timeout: Duration::from_secs(30),
1416 secure: false,
1417 }
1418 }
1419
1420 pub fn host<S: Into<String>>(mut self, host: S) -> Self {
1422 self.host = Some(host.into());
1423 self
1424 }
1425
1426 pub fn port(mut self, port: u16) -> Self {
1428 self.port = port;
1429 self
1430 }
1431
1432 pub fn username<S: Into<String>>(mut self, username: S) -> Self {
1434 self.username = Some(username.into());
1435 self
1436 }
1437
1438 pub fn password<S: Into<String>>(mut self, password: S) -> Self {
1440 self.password = Some(password.into());
1441 self
1442 }
1443
1444 pub fn remote_file<S: Into<String>>(mut self, path: S) -> Self {
1446 self.remote_file = Some(path.into());
1447 self
1448 }
1449
1450 pub fn local_file<P: AsRef<Path>>(mut self, path: P) -> Self {
1452 self.local_file = Some(path.as_ref().to_path_buf());
1453 self
1454 }
1455
1456 pub fn passive_mode(mut self, passive: bool) -> Self {
1458 self.passive_mode = passive;
1459 self
1460 }
1461
1462 pub fn timeout(mut self, timeout: Duration) -> Self {
1464 self.timeout = timeout;
1465 self
1466 }
1467
1468 pub fn secure(mut self, secure: bool) -> Self {
1477 self.secure = secure;
1478 self
1479 }
1480
1481 pub fn build(self) -> Result<FtpGetTasklet, BatchError> {
1483 let host = self
1484 .host
1485 .ok_or_else(|| BatchError::Configuration("FTP host is required".to_string()))?;
1486 let username = self
1487 .username
1488 .ok_or_else(|| BatchError::Configuration("FTP username is required".to_string()))?;
1489 let password = self
1490 .password
1491 .ok_or_else(|| BatchError::Configuration("FTP password is required".to_string()))?;
1492 let remote_file = self
1493 .remote_file
1494 .ok_or_else(|| BatchError::Configuration("Remote file path is required".to_string()))?;
1495 let local_file = self
1496 .local_file
1497 .ok_or_else(|| BatchError::Configuration("Local file path is required".to_string()))?;
1498
1499 let mut tasklet = FtpGetTasklet::new(
1500 &host,
1501 self.port,
1502 &username,
1503 &password,
1504 &remote_file,
1505 &local_file,
1506 )?;
1507
1508 tasklet.set_passive_mode(self.passive_mode);
1509 tasklet.set_timeout(self.timeout);
1510 tasklet.secure = self.secure;
1511
1512 Ok(tasklet)
1513 }
1514}
1515
1516pub struct FtpPutFolderTaskletBuilder {
1518 host: Option<String>,
1519 port: u16,
1520 username: Option<String>,
1521 password: Option<String>,
1522 local_folder: Option<PathBuf>,
1523 remote_folder: Option<String>,
1524 passive_mode: bool,
1525 timeout: Duration,
1526 create_directories: bool,
1527 recursive: bool,
1528 secure: bool,
1529}
1530
1531impl Default for FtpPutFolderTaskletBuilder {
1532 fn default() -> Self {
1533 Self::new()
1534 }
1535}
1536
1537impl FtpPutFolderTaskletBuilder {
1538 pub fn new() -> Self {
1540 Self {
1541 host: None,
1542 port: 21,
1543 username: None,
1544 password: None,
1545 local_folder: None,
1546 remote_folder: None,
1547 passive_mode: true,
1548 timeout: Duration::from_secs(30),
1549 create_directories: true,
1550 recursive: false,
1551 secure: false,
1552 }
1553 }
1554
1555 pub fn host<S: Into<String>>(mut self, host: S) -> Self {
1557 self.host = Some(host.into());
1558 self
1559 }
1560
1561 pub fn port(mut self, port: u16) -> Self {
1563 self.port = port;
1564 self
1565 }
1566
1567 pub fn username<S: Into<String>>(mut self, username: S) -> Self {
1569 self.username = Some(username.into());
1570 self
1571 }
1572
1573 pub fn password<S: Into<String>>(mut self, password: S) -> Self {
1575 self.password = Some(password.into());
1576 self
1577 }
1578
1579 pub fn local_folder<P: AsRef<Path>>(mut self, path: P) -> Self {
1581 self.local_folder = Some(path.as_ref().to_path_buf());
1582 self
1583 }
1584
1585 pub fn remote_folder<S: Into<String>>(mut self, path: S) -> Self {
1587 self.remote_folder = Some(path.into());
1588 self
1589 }
1590
1591 pub fn passive_mode(mut self, passive: bool) -> Self {
1593 self.passive_mode = passive;
1594 self
1595 }
1596
1597 pub fn timeout(mut self, timeout: Duration) -> Self {
1599 self.timeout = timeout;
1600 self
1601 }
1602
1603 pub fn create_directories(mut self, create: bool) -> Self {
1605 self.create_directories = create;
1606 self
1607 }
1608
1609 pub fn recursive(mut self, recursive: bool) -> Self {
1611 self.recursive = recursive;
1612 self
1613 }
1614
1615 pub fn secure(mut self, secure: bool) -> Self {
1624 self.secure = secure;
1625 self
1626 }
1627
1628 pub fn build(self) -> Result<FtpPutFolderTasklet, BatchError> {
1630 let host = self
1631 .host
1632 .ok_or_else(|| BatchError::Configuration("FTP host is required".to_string()))?;
1633 let username = self
1634 .username
1635 .ok_or_else(|| BatchError::Configuration("FTP username is required".to_string()))?;
1636 let password = self
1637 .password
1638 .ok_or_else(|| BatchError::Configuration("FTP password is required".to_string()))?;
1639 let local_folder = self.local_folder.ok_or_else(|| {
1640 BatchError::Configuration("Local folder path is required".to_string())
1641 })?;
1642 let remote_folder = self.remote_folder.ok_or_else(|| {
1643 BatchError::Configuration("Remote folder path is required".to_string())
1644 })?;
1645
1646 let mut tasklet = FtpPutFolderTasklet::new(
1647 &host,
1648 self.port,
1649 &username,
1650 &password,
1651 &local_folder,
1652 &remote_folder,
1653 )?;
1654
1655 tasklet.set_passive_mode(self.passive_mode);
1656 tasklet.set_timeout(self.timeout);
1657 tasklet.set_create_directories(self.create_directories);
1658 tasklet.set_recursive(self.recursive);
1659 tasklet.secure = self.secure;
1660
1661 Ok(tasklet)
1662 }
1663}
1664
1665pub struct FtpGetFolderTaskletBuilder {
1667 host: Option<String>,
1668 port: u16,
1669 username: Option<String>,
1670 password: Option<String>,
1671 remote_folder: Option<String>,
1672 local_folder: Option<PathBuf>,
1673 passive_mode: bool,
1674 timeout: Duration,
1675 create_directories: bool,
1676 recursive: bool,
1677 secure: bool,
1678}
1679
1680impl Default for FtpGetFolderTaskletBuilder {
1681 fn default() -> Self {
1682 Self::new()
1683 }
1684}
1685
1686impl FtpGetFolderTaskletBuilder {
1687 pub fn new() -> Self {
1689 Self {
1690 host: None,
1691 port: 21,
1692 username: None,
1693 password: None,
1694 remote_folder: None,
1695 local_folder: None,
1696 passive_mode: true,
1697 timeout: Duration::from_secs(30),
1698 create_directories: true,
1699 recursive: false,
1700 secure: false,
1701 }
1702 }
1703
1704 pub fn host<S: Into<String>>(mut self, host: S) -> Self {
1706 self.host = Some(host.into());
1707 self
1708 }
1709
1710 pub fn port(mut self, port: u16) -> Self {
1712 self.port = port;
1713 self
1714 }
1715
1716 pub fn username<S: Into<String>>(mut self, username: S) -> Self {
1718 self.username = Some(username.into());
1719 self
1720 }
1721
1722 pub fn password<S: Into<String>>(mut self, password: S) -> Self {
1724 self.password = Some(password.into());
1725 self
1726 }
1727
1728 pub fn remote_folder<S: Into<String>>(mut self, path: S) -> Self {
1730 self.remote_folder = Some(path.into());
1731 self
1732 }
1733
1734 pub fn local_folder<P: AsRef<Path>>(mut self, path: P) -> Self {
1736 self.local_folder = Some(path.as_ref().to_path_buf());
1737 self
1738 }
1739
1740 pub fn passive_mode(mut self, passive: bool) -> Self {
1742 self.passive_mode = passive;
1743 self
1744 }
1745
1746 pub fn timeout(mut self, timeout: Duration) -> Self {
1748 self.timeout = timeout;
1749 self
1750 }
1751
1752 pub fn create_directories(mut self, create: bool) -> Self {
1754 self.create_directories = create;
1755 self
1756 }
1757
1758 pub fn recursive(mut self, recursive: bool) -> Self {
1760 self.recursive = recursive;
1761 self
1762 }
1763
1764 pub fn secure(mut self, secure: bool) -> Self {
1773 self.secure = secure;
1774 self
1775 }
1776
1777 pub fn build(self) -> Result<FtpGetFolderTasklet, BatchError> {
1779 let host = self
1780 .host
1781 .ok_or_else(|| BatchError::Configuration("FTP host is required".to_string()))?;
1782 let username = self
1783 .username
1784 .ok_or_else(|| BatchError::Configuration("FTP username is required".to_string()))?;
1785 let password = self
1786 .password
1787 .ok_or_else(|| BatchError::Configuration("FTP password is required".to_string()))?;
1788 let remote_folder = self.remote_folder.ok_or_else(|| {
1789 BatchError::Configuration("Remote folder path is required".to_string())
1790 })?;
1791 let local_folder = self.local_folder.ok_or_else(|| {
1792 BatchError::Configuration("Local folder path is required".to_string())
1793 })?;
1794
1795 let mut tasklet = FtpGetFolderTasklet::new(
1796 &host,
1797 self.port,
1798 &username,
1799 &password,
1800 &remote_folder,
1801 &local_folder,
1802 )?;
1803
1804 tasklet.set_passive_mode(self.passive_mode);
1805 tasklet.set_timeout(self.timeout);
1806 tasklet.set_create_directories(self.create_directories);
1807 tasklet.set_recursive(self.recursive);
1808 tasklet.secure = self.secure;
1809
1810 Ok(tasklet)
1811 }
1812}
1813
1814#[cfg(test)]
1815mod tests {
1816 use super::*;
1817 use crate::core::step::StepExecution;
1818 use std::env::temp_dir;
1819 use std::fs;
1820 #[test]
1821 fn should_convert_io_error_to_ftp_error() {
1822 use suppaftp::FtpError;
1823 let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
1824 let ftp_err = io_error_to_ftp_error(io_err);
1825 assert!(matches!(ftp_err, FtpError::ConnectionError(_)));
1826 }
1827
1828 #[test]
1829 fn test_ftp_put_tasklet_creation() -> Result<(), BatchError> {
1830 let temp_dir = temp_dir();
1831 let test_file = temp_dir.join("test_upload.txt");
1832 fs::write(&test_file, "test content").unwrap();
1833
1834 let tasklet = FtpPutTasklet::new(
1835 "localhost",
1836 21,
1837 "testuser",
1838 "testpass",
1839 &test_file,
1840 "/remote/test.txt",
1841 )?;
1842
1843 assert_eq!(tasklet.host, "localhost");
1844 assert_eq!(tasklet.port, 21);
1845 assert_eq!(tasklet.username, "testuser");
1846 assert_eq!(tasklet.remote_file, "/remote/test.txt");
1847 assert!(tasklet.passive_mode);
1848
1849 fs::remove_file(&test_file).ok();
1850 Ok(())
1851 }
1852
1853 #[test]
1854 fn test_ftp_get_tasklet_creation() -> Result<(), BatchError> {
1855 let temp_dir = temp_dir();
1856 let local_file = temp_dir.join("downloaded.txt");
1857
1858 let tasklet = FtpGetTasklet::new(
1859 "localhost",
1860 21,
1861 "testuser",
1862 "testpass",
1863 "/remote/test.txt",
1864 &local_file,
1865 )?;
1866
1867 assert_eq!(tasklet.host, "localhost");
1868 assert_eq!(tasklet.port, 21);
1869 assert_eq!(tasklet.username, "testuser");
1870 assert_eq!(tasklet.remote_file, "/remote/test.txt");
1871 assert!(tasklet.passive_mode);
1872
1873 Ok(())
1874 }
1875
1876 #[test]
1877 fn test_ftp_put_builder() -> Result<(), BatchError> {
1878 let temp_dir = temp_dir();
1879 let test_file = temp_dir.join("test_builder.txt");
1880 fs::write(&test_file, "test content").unwrap();
1881
1882 let tasklet = FtpPutTaskletBuilder::new()
1883 .host("ftp.example.com")
1884 .port(2121)
1885 .username("user")
1886 .password("pass")
1887 .local_file(&test_file)
1888 .remote_file("/upload/file.txt")
1889 .passive_mode(false)
1890 .timeout(Duration::from_secs(60))
1891 .build()?;
1892
1893 assert_eq!(tasklet.host, "ftp.example.com");
1894 assert_eq!(tasklet.port, 2121);
1895 assert!(!tasklet.passive_mode);
1896 assert_eq!(tasklet.timeout, Duration::from_secs(60));
1897
1898 fs::remove_file(&test_file).ok();
1899 Ok(())
1900 }
1901
1902 #[test]
1903 fn test_ftp_get_builder() -> Result<(), BatchError> {
1904 let temp_dir = temp_dir();
1905 let local_file = temp_dir.join("download_builder.txt");
1906
1907 let tasklet = FtpGetTaskletBuilder::new()
1908 .host("ftp.example.com")
1909 .port(2121)
1910 .username("user")
1911 .password("pass")
1912 .remote_file("/download/file.txt")
1913 .local_file(&local_file)
1914 .passive_mode(false)
1915 .timeout(Duration::from_secs(60))
1916 .build()?;
1917
1918 assert_eq!(tasklet.host, "ftp.example.com");
1919 assert_eq!(tasklet.port, 2121);
1920 assert!(!tasklet.passive_mode);
1921 assert_eq!(tasklet.timeout, Duration::from_secs(60));
1922
1923 Ok(())
1924 }
1925
1926 #[test]
1927 fn test_builder_validation() {
1928 let result = FtpPutTaskletBuilder::new()
1930 .username("user")
1931 .password("pass")
1932 .build();
1933 assert!(result.is_err());
1934 assert!(
1935 result
1936 .unwrap_err()
1937 .to_string()
1938 .contains("FTP host is required")
1939 );
1940
1941 let result = FtpGetTaskletBuilder::new()
1943 .host("localhost")
1944 .password("pass")
1945 .build();
1946 assert!(result.is_err());
1947 assert!(
1948 result
1949 .unwrap_err()
1950 .to_string()
1951 .contains("FTP username is required")
1952 );
1953
1954 let result = FtpPutTaskletBuilder::new()
1956 .host("localhost")
1957 .username("user")
1958 .build();
1959 assert!(result.is_err());
1960 assert!(
1961 result
1962 .unwrap_err()
1963 .to_string()
1964 .contains("FTP password is required")
1965 );
1966
1967 let result = FtpPutTaskletBuilder::new()
1969 .host("localhost")
1970 .username("user")
1971 .password("pass")
1972 .remote_file("/remote/file.txt")
1973 .build();
1974 assert!(result.is_err());
1975 assert!(
1976 result
1977 .unwrap_err()
1978 .to_string()
1979 .contains("Local file path is required")
1980 );
1981
1982 let result = FtpGetTaskletBuilder::new()
1984 .host("localhost")
1985 .username("user")
1986 .password("pass")
1987 .local_file("/local/file.txt")
1988 .build();
1989 assert!(result.is_err());
1990 assert!(
1991 result
1992 .unwrap_err()
1993 .to_string()
1994 .contains("Remote file path is required")
1995 );
1996 }
1997
1998 #[test]
1999 fn test_nonexistent_local_file() {
2000 let result = FtpPutTasklet::new(
2001 "localhost",
2002 21,
2003 "user",
2004 "pass",
2005 "/nonexistent/file.txt",
2006 "/remote/file.txt",
2007 );
2008 assert!(result.is_err());
2009 assert!(
2010 result
2011 .unwrap_err()
2012 .to_string()
2013 .contains("Local file does not exist")
2014 );
2015 }
2016
2017 #[test]
2018 fn test_ftp_put_tasklet_configuration() -> Result<(), BatchError> {
2019 let temp_dir = temp_dir();
2020 let test_file = temp_dir.join("config_test.txt");
2021 fs::write(&test_file, "test content").unwrap();
2022
2023 let mut tasklet = FtpPutTasklet::new(
2024 "localhost",
2025 21,
2026 "user",
2027 "pass",
2028 &test_file,
2029 "/remote/file.txt",
2030 )?;
2031
2032 assert!(tasklet.passive_mode);
2034 assert_eq!(tasklet.timeout, Duration::from_secs(30));
2035
2036 tasklet.set_passive_mode(false);
2038 tasklet.set_timeout(Duration::from_secs(60));
2039
2040 assert!(!tasklet.passive_mode);
2041 assert_eq!(tasklet.timeout, Duration::from_secs(60));
2042
2043 fs::remove_file(&test_file).ok();
2044 Ok(())
2045 }
2046
2047 #[test]
2048 fn test_ftp_get_tasklet_configuration() -> Result<(), BatchError> {
2049 let temp_dir = temp_dir();
2050 let local_file = temp_dir.join("config_test.txt");
2051
2052 let mut tasklet = FtpGetTasklet::new(
2053 "localhost",
2054 21,
2055 "user",
2056 "pass",
2057 "/remote/file.txt",
2058 &local_file,
2059 )?;
2060
2061 assert!(tasklet.passive_mode);
2063 assert_eq!(tasklet.timeout, Duration::from_secs(30));
2064
2065 tasklet.set_passive_mode(false);
2067 tasklet.set_timeout(Duration::from_secs(120));
2068
2069 assert!(!tasklet.passive_mode);
2070 assert_eq!(tasklet.timeout, Duration::from_secs(120));
2071
2072 Ok(())
2073 }
2074
2075 #[test]
2076 fn test_ftp_put_tasklet_execution_with_connection_error() {
2077 let temp_dir = temp_dir();
2078 let test_file = temp_dir.join("connection_error_test.txt");
2079 fs::write(&test_file, "test content").unwrap();
2080
2081 let tasklet = FtpPutTasklet::new(
2082 "nonexistent.host.invalid",
2083 21,
2084 "user",
2085 "pass",
2086 &test_file,
2087 "/remote/file.txt",
2088 )
2089 .unwrap();
2090
2091 let step_execution = StepExecution::new("test-step");
2092 let result = tasklet.execute(&step_execution);
2093
2094 assert!(result.is_err());
2095 let error = result.unwrap_err();
2096 assert!(matches!(error, BatchError::Io(_)));
2097 assert!(
2098 error
2099 .to_string()
2100 .contains("Failed to connect to FTP server")
2101 );
2102
2103 fs::remove_file(&test_file).ok();
2104 }
2105
2106 #[test]
2107 fn test_ftp_get_tasklet_execution_with_connection_error() {
2108 let temp_dir = temp_dir();
2109 let local_file = temp_dir.join("connection_error_test.txt");
2110
2111 let tasklet = FtpGetTasklet::new(
2112 "nonexistent.host.invalid",
2113 21,
2114 "user",
2115 "pass",
2116 "/remote/file.txt",
2117 &local_file,
2118 )
2119 .unwrap();
2120
2121 let step_execution = StepExecution::new("test-step");
2122 let result = tasklet.execute(&step_execution);
2123
2124 assert!(result.is_err());
2125 let error = result.unwrap_err();
2126 assert!(matches!(error, BatchError::Io(_)));
2127 assert!(
2128 error
2129 .to_string()
2130 .contains("Failed to connect to FTP server")
2131 );
2132 }
2133
2134 #[test]
2135 fn test_ftp_put_tasklet_secure_execution_with_connection_error() {
2136 let temp_dir = temp_dir();
2137 let test_file = temp_dir.join("secure_conn_error_test.txt");
2138 fs::write(&test_file, "test content").unwrap();
2139
2140 let tasklet = FtpPutTaskletBuilder::new()
2141 .host("nonexistent.host.invalid")
2142 .port(990)
2143 .username("user")
2144 .password("pass")
2145 .local_file(&test_file)
2146 .remote_file("/secure/file.txt")
2147 .secure(true)
2148 .build()
2149 .unwrap();
2150
2151 let step_execution = StepExecution::new("test-step");
2152 let result = tasklet.execute(&step_execution);
2153 assert!(result.is_err());
2154 assert!(matches!(result.unwrap_err(), BatchError::Io(_)));
2155
2156 fs::remove_file(&test_file).ok();
2157 }
2158
2159 #[test]
2160 fn test_ftp_get_tasklet_secure_execution_with_connection_error() {
2161 let temp_dir = temp_dir();
2162 let local_file = temp_dir.join("secure_get_conn_error_test.txt");
2163
2164 let tasklet = FtpGetTaskletBuilder::new()
2165 .host("nonexistent.host.invalid")
2166 .port(990)
2167 .username("user")
2168 .password("pass")
2169 .remote_file("/secure/file.txt")
2170 .local_file(&local_file)
2171 .secure(true)
2172 .build()
2173 .unwrap();
2174
2175 let step_execution = StepExecution::new("test-step");
2176 let result = tasklet.execute(&step_execution);
2177 assert!(result.is_err());
2178 assert!(matches!(result.unwrap_err(), BatchError::Io(_)));
2179 }
2180
2181 #[test]
2182 fn test_ftp_put_folder_tasklet_creation() -> Result<(), BatchError> {
2183 let temp_dir = temp_dir();
2184 let test_folder = temp_dir.join("test_upload_folder");
2185 fs::create_dir_all(&test_folder).unwrap();
2186 fs::write(test_folder.join("file1.txt"), "content1").unwrap();
2187 fs::write(test_folder.join("file2.txt"), "content2").unwrap();
2188
2189 let tasklet = FtpPutFolderTasklet::new(
2190 "localhost",
2191 21,
2192 "testuser",
2193 "testpass",
2194 &test_folder,
2195 "/remote/folder",
2196 )?;
2197
2198 assert_eq!(tasklet.host, "localhost");
2199 assert_eq!(tasklet.port, 21);
2200 assert_eq!(tasklet.username, "testuser");
2201 assert_eq!(tasklet.remote_folder, "/remote/folder");
2202 assert!(tasklet.passive_mode);
2203 assert!(tasklet.create_directories);
2204 assert!(!tasklet.recursive);
2205
2206 fs::remove_dir_all(&test_folder).ok();
2207 Ok(())
2208 }
2209
2210 #[test]
2211 fn test_ftp_get_folder_tasklet_creation() -> Result<(), BatchError> {
2212 let temp_dir = temp_dir();
2213 let local_folder = temp_dir.join("download_folder");
2214
2215 let tasklet = FtpGetFolderTasklet::new(
2216 "localhost",
2217 21,
2218 "testuser",
2219 "testpass",
2220 "/remote/folder",
2221 &local_folder,
2222 )?;
2223
2224 assert_eq!(tasklet.host, "localhost");
2225 assert_eq!(tasklet.port, 21);
2226 assert_eq!(tasklet.username, "testuser");
2227 assert_eq!(tasklet.remote_folder, "/remote/folder");
2228 assert!(tasklet.passive_mode);
2229 assert!(tasklet.create_directories);
2230 assert!(!tasklet.recursive);
2231
2232 Ok(())
2233 }
2234
2235 #[test]
2236 fn test_ftp_put_folder_builder() -> Result<(), BatchError> {
2237 let temp_dir = temp_dir();
2238 let test_folder = temp_dir.join("test_builder_folder");
2239 fs::create_dir_all(&test_folder).unwrap();
2240 fs::write(test_folder.join("file.txt"), "content").unwrap();
2241
2242 let tasklet = FtpPutFolderTaskletBuilder::new()
2243 .host("ftp.example.com")
2244 .port(2121)
2245 .username("user")
2246 .password("pass")
2247 .local_folder(&test_folder)
2248 .remote_folder("/upload/folder")
2249 .passive_mode(false)
2250 .timeout(Duration::from_secs(60))
2251 .create_directories(false)
2252 .recursive(true)
2253 .build()?;
2254
2255 assert_eq!(tasklet.host, "ftp.example.com");
2256 assert_eq!(tasklet.port, 2121);
2257 assert!(!tasklet.passive_mode);
2258 assert_eq!(tasklet.timeout, Duration::from_secs(60));
2259 assert!(!tasklet.create_directories);
2260 assert!(tasklet.recursive);
2261
2262 fs::remove_dir_all(&test_folder).ok();
2263 Ok(())
2264 }
2265
2266 #[test]
2267 fn test_ftp_get_folder_builder() -> Result<(), BatchError> {
2268 let temp_dir = temp_dir();
2269 let local_folder = temp_dir.join("download_builder_folder");
2270
2271 let tasklet = FtpGetFolderTaskletBuilder::new()
2272 .host("ftp.example.com")
2273 .port(2121)
2274 .username("user")
2275 .password("pass")
2276 .remote_folder("/download/folder")
2277 .local_folder(&local_folder)
2278 .passive_mode(false)
2279 .timeout(Duration::from_secs(60))
2280 .create_directories(false)
2281 .recursive(true)
2282 .build()?;
2283
2284 assert_eq!(tasklet.host, "ftp.example.com");
2285 assert_eq!(tasklet.port, 2121);
2286 assert!(!tasklet.passive_mode);
2287 assert_eq!(tasklet.timeout, Duration::from_secs(60));
2288 assert!(!tasklet.create_directories);
2289 assert!(tasklet.recursive);
2290
2291 Ok(())
2292 }
2293
2294 #[test]
2295 fn test_folder_builder_validation() {
2296 let result = FtpPutFolderTaskletBuilder::new()
2298 .username("user")
2299 .password("pass")
2300 .build();
2301 assert!(result.is_err());
2302 assert!(
2303 result
2304 .unwrap_err()
2305 .to_string()
2306 .contains("FTP host is required")
2307 );
2308
2309 let result = FtpGetFolderTaskletBuilder::new()
2311 .host("localhost")
2312 .password("pass")
2313 .build();
2314 assert!(result.is_err());
2315 assert!(
2316 result
2317 .unwrap_err()
2318 .to_string()
2319 .contains("FTP username is required")
2320 );
2321
2322 let result = FtpPutFolderTaskletBuilder::new()
2324 .host("localhost")
2325 .username("user")
2326 .password("pass")
2327 .remote_folder("/remote/folder")
2328 .build();
2329 assert!(result.is_err());
2330 assert!(
2331 result
2332 .unwrap_err()
2333 .to_string()
2334 .contains("Local folder path is required")
2335 );
2336
2337 let result = FtpGetFolderTaskletBuilder::new()
2339 .host("localhost")
2340 .username("user")
2341 .password("pass")
2342 .local_folder("/local/folder")
2343 .build();
2344 assert!(result.is_err());
2345 assert!(
2346 result
2347 .unwrap_err()
2348 .to_string()
2349 .contains("Remote folder path is required")
2350 );
2351 }
2352
2353 #[test]
2354 fn test_nonexistent_local_folder() {
2355 let result = FtpPutFolderTasklet::new(
2356 "localhost",
2357 21,
2358 "user",
2359 "pass",
2360 "/nonexistent/folder",
2361 "/remote/folder",
2362 );
2363 assert!(result.is_err());
2364 assert!(
2365 result
2366 .unwrap_err()
2367 .to_string()
2368 .contains("Local folder does not exist")
2369 );
2370 }
2371
2372 #[test]
2373 fn test_local_file_not_directory() {
2374 let temp_dir = temp_dir();
2375 let test_file = temp_dir.join("not_a_directory.txt");
2376 fs::write(&test_file, "content").unwrap();
2377
2378 let result = FtpPutFolderTasklet::new(
2379 "localhost",
2380 21,
2381 "user",
2382 "pass",
2383 &test_file,
2384 "/remote/folder",
2385 );
2386 assert!(result.is_err());
2387 assert!(
2388 result
2389 .unwrap_err()
2390 .to_string()
2391 .contains("Local path is not a directory")
2392 );
2393
2394 fs::remove_file(&test_file).ok();
2395 }
2396
2397 #[test]
2398 fn test_ftp_put_folder_tasklet_configuration() -> Result<(), BatchError> {
2399 let temp_dir = temp_dir();
2400 let test_folder = temp_dir.join("config_folder_test");
2401 fs::create_dir_all(&test_folder).unwrap();
2402 fs::write(test_folder.join("file.txt"), "content").unwrap();
2403
2404 let mut tasklet = FtpPutFolderTasklet::new(
2405 "localhost",
2406 21,
2407 "user",
2408 "pass",
2409 &test_folder,
2410 "/remote/folder",
2411 )?;
2412
2413 assert!(tasklet.passive_mode);
2415 assert_eq!(tasklet.timeout, Duration::from_secs(30));
2416 assert!(tasklet.create_directories);
2417 assert!(!tasklet.recursive);
2418
2419 tasklet.set_passive_mode(false);
2421 tasklet.set_timeout(Duration::from_secs(90));
2422 tasklet.set_create_directories(false);
2423 tasklet.set_recursive(true);
2424
2425 assert!(!tasklet.passive_mode);
2426 assert_eq!(tasklet.timeout, Duration::from_secs(90));
2427 assert!(!tasklet.create_directories);
2428 assert!(tasklet.recursive);
2429
2430 fs::remove_dir_all(&test_folder).ok();
2431 Ok(())
2432 }
2433
2434 #[test]
2435 fn test_ftp_get_folder_tasklet_configuration() -> Result<(), BatchError> {
2436 let temp_dir = temp_dir();
2437 let local_folder = temp_dir.join("config_folder_test");
2438
2439 let mut tasklet = FtpGetFolderTasklet::new(
2440 "localhost",
2441 21,
2442 "user",
2443 "pass",
2444 "/remote/folder",
2445 &local_folder,
2446 )?;
2447
2448 assert!(tasklet.passive_mode);
2450 assert_eq!(tasklet.timeout, Duration::from_secs(30));
2451 assert!(tasklet.create_directories);
2452 assert!(!tasklet.recursive);
2453
2454 tasklet.set_passive_mode(false);
2456 tasklet.set_timeout(Duration::from_secs(180));
2457 tasklet.set_create_directories(false);
2458 tasklet.set_recursive(true);
2459
2460 assert!(!tasklet.passive_mode);
2461 assert_eq!(tasklet.timeout, Duration::from_secs(180));
2462 assert!(!tasklet.create_directories);
2463 assert!(tasklet.recursive);
2464
2465 Ok(())
2466 }
2467
2468 #[test]
2469 fn test_ftp_put_folder_tasklet_execution_with_connection_error() {
2470 let temp_dir = temp_dir();
2471 let test_folder = temp_dir.join("connection_error_folder_test");
2472 fs::create_dir_all(&test_folder).unwrap();
2473 fs::write(test_folder.join("file.txt"), "content").unwrap();
2474
2475 let tasklet = FtpPutFolderTasklet::new(
2476 "nonexistent.host.invalid",
2477 21,
2478 "user",
2479 "pass",
2480 &test_folder,
2481 "/remote/folder",
2482 )
2483 .unwrap();
2484
2485 let step_execution = StepExecution::new("test-step");
2486 let result = tasklet.execute(&step_execution);
2487
2488 assert!(result.is_err());
2489 let error = result.unwrap_err();
2490 assert!(matches!(error, BatchError::Io(_)));
2491 assert!(
2492 error
2493 .to_string()
2494 .contains("Failed to connect to FTP server")
2495 );
2496
2497 fs::remove_dir_all(&test_folder).ok();
2498 }
2499
2500 #[test]
2501 fn test_ftp_get_folder_tasklet_execution_with_connection_error() {
2502 let temp_dir = temp_dir();
2503 let local_folder = temp_dir.join("connection_error_folder_test");
2504
2505 let tasklet = FtpGetFolderTasklet::new(
2506 "nonexistent.host.invalid",
2507 21,
2508 "user",
2509 "pass",
2510 "/remote/folder",
2511 &local_folder,
2512 )
2513 .unwrap();
2514
2515 let step_execution = StepExecution::new("test-step");
2516 let result = tasklet.execute(&step_execution);
2517
2518 assert!(result.is_err());
2519 let error = result.unwrap_err();
2520 assert!(matches!(error, BatchError::Io(_)));
2521 assert!(
2522 error
2523 .to_string()
2524 .contains("Failed to connect to FTP server")
2525 );
2526 }
2527
2528 #[test]
2529 fn test_builder_default_implementations() {
2530 let _put_builder = FtpPutTaskletBuilder::default();
2532 let _get_builder = FtpGetTaskletBuilder::default();
2533 let _put_folder_builder = FtpPutFolderTaskletBuilder::default();
2534 let _get_folder_builder = FtpGetFolderTaskletBuilder::default();
2535 }
2536
2537 #[test]
2538 fn test_builder_fluent_interface() -> Result<(), BatchError> {
2539 let temp_dir = temp_dir();
2540 let test_file = temp_dir.join("fluent_test.txt");
2541 fs::write(&test_file, "test content").unwrap();
2542
2543 let tasklet = FtpPutTaskletBuilder::new()
2545 .host("example.com")
2546 .port(2121)
2547 .username("testuser")
2548 .password("testpass")
2549 .local_file(&test_file)
2550 .remote_file("/remote/test.txt")
2551 .passive_mode(true)
2552 .timeout(Duration::from_secs(45))
2553 .build()?;
2554
2555 assert_eq!(tasklet.host, "example.com");
2556 assert_eq!(tasklet.port, 2121);
2557 assert_eq!(tasklet.username, "testuser");
2558 assert_eq!(tasklet.password, "testpass");
2559 assert_eq!(tasklet.remote_file, "/remote/test.txt");
2560 assert!(tasklet.passive_mode);
2561 assert_eq!(tasklet.timeout, Duration::from_secs(45));
2562
2563 fs::remove_file(&test_file).ok();
2564 Ok(())
2565 }
2566
2567 #[test]
2568 fn test_error_message_quality() {
2569 let result = FtpPutTaskletBuilder::new().build();
2571 assert!(result.is_err());
2572 let error_msg = result.unwrap_err().to_string();
2573 assert!(error_msg.contains("FTP host is required"));
2574
2575 let result = FtpPutTaskletBuilder::new().host("localhost").build();
2576 assert!(result.is_err());
2577 let error_msg = result.unwrap_err().to_string();
2578 assert!(error_msg.contains("FTP username is required"));
2579 }
2580
2581 #[test]
2582 fn test_path_handling() -> Result<(), BatchError> {
2583 let temp_dir = temp_dir();
2584 let test_file = temp_dir.join("path_test.txt");
2585 fs::write(&test_file, "test content").unwrap();
2586
2587 let tasklet1 = FtpPutTasklet::new(
2589 "localhost",
2590 21,
2591 "user",
2592 "pass",
2593 &test_file,
2594 "/remote/file.txt",
2595 )?;
2596
2597 let tasklet2 = FtpPutTasklet::new(
2598 "localhost",
2599 21,
2600 "user",
2601 "pass",
2602 test_file.as_path(),
2603 "/remote/file.txt",
2604 )?;
2605
2606 assert_eq!(tasklet1.local_file, tasklet2.local_file);
2607
2608 fs::remove_file(&test_file).ok();
2609 Ok(())
2610 }
2611
2612 #[test]
2613 fn test_timeout_configuration() -> Result<(), BatchError> {
2614 let temp_dir = temp_dir();
2615 let test_file = temp_dir.join("timeout_test.txt");
2616 fs::write(&test_file, "test content").unwrap();
2617
2618 let tasklet = FtpPutTaskletBuilder::new()
2620 .host("localhost")
2621 .username("user")
2622 .password("pass")
2623 .local_file(&test_file)
2624 .remote_file("/remote/file.txt")
2625 .timeout(Duration::from_millis(500))
2626 .build()?;
2627
2628 assert_eq!(tasklet.timeout, Duration::from_millis(500));
2629
2630 let tasklet = FtpPutTaskletBuilder::new()
2631 .host("localhost")
2632 .username("user")
2633 .password("pass")
2634 .local_file(&test_file)
2635 .remote_file("/remote/file.txt")
2636 .timeout(Duration::from_secs(300))
2637 .build()?;
2638
2639 assert_eq!(tasklet.timeout, Duration::from_secs(300));
2640
2641 fs::remove_file(&test_file).ok();
2642 Ok(())
2643 }
2644
2645 #[test]
2646 fn test_port_configuration() -> Result<(), BatchError> {
2647 let temp_dir = temp_dir();
2648 let test_file = temp_dir.join("port_test.txt");
2649 fs::write(&test_file, "test content").unwrap();
2650
2651 let tasklet = FtpPutTaskletBuilder::new()
2653 .host("localhost")
2654 .port(990) .username("user")
2656 .password("pass")
2657 .local_file(&test_file)
2658 .remote_file("/remote/file.txt")
2659 .build()?;
2660
2661 assert_eq!(tasklet.port, 990);
2662
2663 let tasklet = FtpPutTaskletBuilder::new()
2664 .host("localhost")
2665 .port(2121) .username("user")
2667 .password("pass")
2668 .local_file(&test_file)
2669 .remote_file("/remote/file.txt")
2670 .build()?;
2671
2672 assert_eq!(tasklet.port, 2121);
2673
2674 fs::remove_file(&test_file).ok();
2675 Ok(())
2676 }
2677
2678 #[test]
2679 fn test_passive_mode_configuration() -> Result<(), BatchError> {
2680 let temp_dir = temp_dir();
2681 let test_file = temp_dir.join("passive_test.txt");
2682 fs::write(&test_file, "test content").unwrap();
2683
2684 let tasklet = FtpPutTaskletBuilder::new()
2686 .host("localhost")
2687 .username("user")
2688 .password("pass")
2689 .local_file(&test_file)
2690 .remote_file("/remote/file.txt")
2691 .passive_mode(true)
2692 .build()?;
2693
2694 assert!(tasklet.passive_mode);
2695
2696 let tasklet = FtpPutTaskletBuilder::new()
2698 .host("localhost")
2699 .username("user")
2700 .password("pass")
2701 .local_file(&test_file)
2702 .remote_file("/remote/file.txt")
2703 .passive_mode(false)
2704 .build()?;
2705
2706 assert!(!tasklet.passive_mode);
2707
2708 fs::remove_file(&test_file).ok();
2709 Ok(())
2710 }
2711
2712 #[test]
2713 fn test_secure_ftp_configuration() -> Result<(), BatchError> {
2714 let temp_dir = temp_dir();
2715 let test_file = temp_dir.join("secure_test.txt");
2716 fs::write(&test_file, "test content").unwrap();
2717
2718 let tasklet = FtpPutTaskletBuilder::new()
2720 .host("localhost")
2721 .username("user")
2722 .password("pass")
2723 .local_file(&test_file)
2724 .remote_file("/remote/file.txt")
2725 .build()?;
2726
2727 assert!(!tasklet.secure);
2728
2729 let tasklet = FtpPutTaskletBuilder::new()
2731 .host("secure-ftp.example.com")
2732 .port(990)
2733 .username("user")
2734 .password("pass")
2735 .local_file(&test_file)
2736 .remote_file("/secure/file.txt")
2737 .secure(true)
2738 .build()?;
2739
2740 assert!(tasklet.secure);
2741 assert_eq!(tasklet.port, 990);
2742
2743 let local_file = temp_dir.join("downloaded_secure.txt");
2745 let get_tasklet = FtpGetTaskletBuilder::new()
2746 .host("secure-ftp.example.com")
2747 .port(990)
2748 .username("user")
2749 .password("pass")
2750 .remote_file("/secure/file.txt")
2751 .local_file(&local_file)
2752 .secure(true)
2753 .build()?;
2754
2755 assert!(get_tasklet.secure);
2756 assert_eq!(get_tasklet.port, 990);
2757
2758 fs::remove_file(&test_file).ok();
2759 Ok(())
2760 }
2761
2762 #[test]
2763 fn test_secure_ftp_folder_configuration() -> Result<(), BatchError> {
2764 let temp_dir = temp_dir();
2765 let test_folder = temp_dir.join("secure_folder_test");
2766 fs::create_dir_all(&test_folder).unwrap();
2767 fs::write(test_folder.join("file.txt"), "test content").unwrap();
2768
2769 let tasklet = FtpPutFolderTaskletBuilder::new()
2771 .host("localhost")
2772 .username("user")
2773 .password("pass")
2774 .local_folder(&test_folder)
2775 .remote_folder("/remote/folder")
2776 .build()?;
2777
2778 assert!(!tasklet.secure);
2779
2780 let tasklet = FtpPutFolderTaskletBuilder::new()
2782 .host("secure-ftp.example.com")
2783 .port(990)
2784 .username("user")
2785 .password("pass")
2786 .local_folder(&test_folder)
2787 .remote_folder("/secure/folder")
2788 .secure(true)
2789 .build()?;
2790
2791 assert!(tasklet.secure);
2792 assert_eq!(tasklet.port, 990);
2793
2794 let local_folder = temp_dir.join("downloaded_secure_folder");
2796 let get_tasklet = FtpGetFolderTaskletBuilder::new()
2797 .host("secure-ftp.example.com")
2798 .port(990)
2799 .username("user")
2800 .password("pass")
2801 .remote_folder("/secure/folder")
2802 .local_folder(&local_folder)
2803 .secure(true)
2804 .build()?;
2805
2806 assert!(get_tasklet.secure);
2807 assert_eq!(get_tasklet.port, 990);
2808
2809 fs::remove_dir_all(&test_folder).ok();
2810 Ok(())
2811 }
2812}