1use crate::{
113 core::step::{RepeatStatus, StepExecution, Tasklet},
114 BatchError,
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 if !parent.exists() {
488 std::fs::create_dir_all(parent).map_err(BatchError::Io)?;
489 }
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 if let Some(parent) = local_path.parent() {
1045 fs::create_dir_all(parent).map_err(BatchError::Io)?;
1046 }
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 if let Some(parent) = local_path.parent() {
1138 fs::create_dir_all(parent).map_err(BatchError::Io)?;
1139 }
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!(result
1935 .unwrap_err()
1936 .to_string()
1937 .contains("FTP host is required"));
1938
1939 let result = FtpGetTaskletBuilder::new()
1941 .host("localhost")
1942 .password("pass")
1943 .build();
1944 assert!(result.is_err());
1945 assert!(result
1946 .unwrap_err()
1947 .to_string()
1948 .contains("FTP username is required"));
1949
1950 let result = FtpPutTaskletBuilder::new()
1952 .host("localhost")
1953 .username("user")
1954 .build();
1955 assert!(result.is_err());
1956 assert!(result
1957 .unwrap_err()
1958 .to_string()
1959 .contains("FTP password is required"));
1960
1961 let result = FtpPutTaskletBuilder::new()
1963 .host("localhost")
1964 .username("user")
1965 .password("pass")
1966 .remote_file("/remote/file.txt")
1967 .build();
1968 assert!(result.is_err());
1969 assert!(result
1970 .unwrap_err()
1971 .to_string()
1972 .contains("Local file path is required"));
1973
1974 let result = FtpGetTaskletBuilder::new()
1976 .host("localhost")
1977 .username("user")
1978 .password("pass")
1979 .local_file("/local/file.txt")
1980 .build();
1981 assert!(result.is_err());
1982 assert!(result
1983 .unwrap_err()
1984 .to_string()
1985 .contains("Remote file path is required"));
1986 }
1987
1988 #[test]
1989 fn test_nonexistent_local_file() {
1990 let result = FtpPutTasklet::new(
1991 "localhost",
1992 21,
1993 "user",
1994 "pass",
1995 "/nonexistent/file.txt",
1996 "/remote/file.txt",
1997 );
1998 assert!(result.is_err());
1999 assert!(result
2000 .unwrap_err()
2001 .to_string()
2002 .contains("Local file does not exist"));
2003 }
2004
2005 #[test]
2006 fn test_ftp_put_tasklet_configuration() -> Result<(), BatchError> {
2007 let temp_dir = temp_dir();
2008 let test_file = temp_dir.join("config_test.txt");
2009 fs::write(&test_file, "test content").unwrap();
2010
2011 let mut tasklet = FtpPutTasklet::new(
2012 "localhost",
2013 21,
2014 "user",
2015 "pass",
2016 &test_file,
2017 "/remote/file.txt",
2018 )?;
2019
2020 assert!(tasklet.passive_mode);
2022 assert_eq!(tasklet.timeout, Duration::from_secs(30));
2023
2024 tasklet.set_passive_mode(false);
2026 tasklet.set_timeout(Duration::from_secs(60));
2027
2028 assert!(!tasklet.passive_mode);
2029 assert_eq!(tasklet.timeout, Duration::from_secs(60));
2030
2031 fs::remove_file(&test_file).ok();
2032 Ok(())
2033 }
2034
2035 #[test]
2036 fn test_ftp_get_tasklet_configuration() -> Result<(), BatchError> {
2037 let temp_dir = temp_dir();
2038 let local_file = temp_dir.join("config_test.txt");
2039
2040 let mut tasklet = FtpGetTasklet::new(
2041 "localhost",
2042 21,
2043 "user",
2044 "pass",
2045 "/remote/file.txt",
2046 &local_file,
2047 )?;
2048
2049 assert!(tasklet.passive_mode);
2051 assert_eq!(tasklet.timeout, Duration::from_secs(30));
2052
2053 tasklet.set_passive_mode(false);
2055 tasklet.set_timeout(Duration::from_secs(120));
2056
2057 assert!(!tasklet.passive_mode);
2058 assert_eq!(tasklet.timeout, Duration::from_secs(120));
2059
2060 Ok(())
2061 }
2062
2063 #[test]
2064 fn test_ftp_put_tasklet_execution_with_connection_error() {
2065 let temp_dir = temp_dir();
2066 let test_file = temp_dir.join("connection_error_test.txt");
2067 fs::write(&test_file, "test content").unwrap();
2068
2069 let tasklet = FtpPutTasklet::new(
2070 "nonexistent.host.invalid",
2071 21,
2072 "user",
2073 "pass",
2074 &test_file,
2075 "/remote/file.txt",
2076 )
2077 .unwrap();
2078
2079 let step_execution = StepExecution::new("test-step");
2080 let result = tasklet.execute(&step_execution);
2081
2082 assert!(result.is_err());
2083 let error = result.unwrap_err();
2084 assert!(matches!(error, BatchError::Io(_)));
2085 assert!(error
2086 .to_string()
2087 .contains("Failed to connect to FTP server"));
2088
2089 fs::remove_file(&test_file).ok();
2090 }
2091
2092 #[test]
2093 fn test_ftp_get_tasklet_execution_with_connection_error() {
2094 let temp_dir = temp_dir();
2095 let local_file = temp_dir.join("connection_error_test.txt");
2096
2097 let tasklet = FtpGetTasklet::new(
2098 "nonexistent.host.invalid",
2099 21,
2100 "user",
2101 "pass",
2102 "/remote/file.txt",
2103 &local_file,
2104 )
2105 .unwrap();
2106
2107 let step_execution = StepExecution::new("test-step");
2108 let result = tasklet.execute(&step_execution);
2109
2110 assert!(result.is_err());
2111 let error = result.unwrap_err();
2112 assert!(matches!(error, BatchError::Io(_)));
2113 assert!(error
2114 .to_string()
2115 .contains("Failed to connect to FTP server"));
2116 }
2117
2118 #[test]
2119 fn test_ftp_put_tasklet_secure_execution_with_connection_error() {
2120 let temp_dir = temp_dir();
2121 let test_file = temp_dir.join("secure_conn_error_test.txt");
2122 fs::write(&test_file, "test content").unwrap();
2123
2124 let tasklet = FtpPutTaskletBuilder::new()
2125 .host("nonexistent.host.invalid")
2126 .port(990)
2127 .username("user")
2128 .password("pass")
2129 .local_file(&test_file)
2130 .remote_file("/secure/file.txt")
2131 .secure(true)
2132 .build()
2133 .unwrap();
2134
2135 let step_execution = StepExecution::new("test-step");
2136 let result = tasklet.execute(&step_execution);
2137 assert!(result.is_err());
2138 assert!(matches!(result.unwrap_err(), BatchError::Io(_)));
2139
2140 fs::remove_file(&test_file).ok();
2141 }
2142
2143 #[test]
2144 fn test_ftp_get_tasklet_secure_execution_with_connection_error() {
2145 let temp_dir = temp_dir();
2146 let local_file = temp_dir.join("secure_get_conn_error_test.txt");
2147
2148 let tasklet = FtpGetTaskletBuilder::new()
2149 .host("nonexistent.host.invalid")
2150 .port(990)
2151 .username("user")
2152 .password("pass")
2153 .remote_file("/secure/file.txt")
2154 .local_file(&local_file)
2155 .secure(true)
2156 .build()
2157 .unwrap();
2158
2159 let step_execution = StepExecution::new("test-step");
2160 let result = tasklet.execute(&step_execution);
2161 assert!(result.is_err());
2162 assert!(matches!(result.unwrap_err(), BatchError::Io(_)));
2163 }
2164
2165 #[test]
2166 fn test_ftp_put_folder_tasklet_creation() -> Result<(), BatchError> {
2167 let temp_dir = temp_dir();
2168 let test_folder = temp_dir.join("test_upload_folder");
2169 fs::create_dir_all(&test_folder).unwrap();
2170 fs::write(test_folder.join("file1.txt"), "content1").unwrap();
2171 fs::write(test_folder.join("file2.txt"), "content2").unwrap();
2172
2173 let tasklet = FtpPutFolderTasklet::new(
2174 "localhost",
2175 21,
2176 "testuser",
2177 "testpass",
2178 &test_folder,
2179 "/remote/folder",
2180 )?;
2181
2182 assert_eq!(tasklet.host, "localhost");
2183 assert_eq!(tasklet.port, 21);
2184 assert_eq!(tasklet.username, "testuser");
2185 assert_eq!(tasklet.remote_folder, "/remote/folder");
2186 assert!(tasklet.passive_mode);
2187 assert!(tasklet.create_directories);
2188 assert!(!tasklet.recursive);
2189
2190 fs::remove_dir_all(&test_folder).ok();
2191 Ok(())
2192 }
2193
2194 #[test]
2195 fn test_ftp_get_folder_tasklet_creation() -> Result<(), BatchError> {
2196 let temp_dir = temp_dir();
2197 let local_folder = temp_dir.join("download_folder");
2198
2199 let tasklet = FtpGetFolderTasklet::new(
2200 "localhost",
2201 21,
2202 "testuser",
2203 "testpass",
2204 "/remote/folder",
2205 &local_folder,
2206 )?;
2207
2208 assert_eq!(tasklet.host, "localhost");
2209 assert_eq!(tasklet.port, 21);
2210 assert_eq!(tasklet.username, "testuser");
2211 assert_eq!(tasklet.remote_folder, "/remote/folder");
2212 assert!(tasklet.passive_mode);
2213 assert!(tasklet.create_directories);
2214 assert!(!tasklet.recursive);
2215
2216 Ok(())
2217 }
2218
2219 #[test]
2220 fn test_ftp_put_folder_builder() -> Result<(), BatchError> {
2221 let temp_dir = temp_dir();
2222 let test_folder = temp_dir.join("test_builder_folder");
2223 fs::create_dir_all(&test_folder).unwrap();
2224 fs::write(test_folder.join("file.txt"), "content").unwrap();
2225
2226 let tasklet = FtpPutFolderTaskletBuilder::new()
2227 .host("ftp.example.com")
2228 .port(2121)
2229 .username("user")
2230 .password("pass")
2231 .local_folder(&test_folder)
2232 .remote_folder("/upload/folder")
2233 .passive_mode(false)
2234 .timeout(Duration::from_secs(60))
2235 .create_directories(false)
2236 .recursive(true)
2237 .build()?;
2238
2239 assert_eq!(tasklet.host, "ftp.example.com");
2240 assert_eq!(tasklet.port, 2121);
2241 assert!(!tasklet.passive_mode);
2242 assert_eq!(tasklet.timeout, Duration::from_secs(60));
2243 assert!(!tasklet.create_directories);
2244 assert!(tasklet.recursive);
2245
2246 fs::remove_dir_all(&test_folder).ok();
2247 Ok(())
2248 }
2249
2250 #[test]
2251 fn test_ftp_get_folder_builder() -> Result<(), BatchError> {
2252 let temp_dir = temp_dir();
2253 let local_folder = temp_dir.join("download_builder_folder");
2254
2255 let tasklet = FtpGetFolderTaskletBuilder::new()
2256 .host("ftp.example.com")
2257 .port(2121)
2258 .username("user")
2259 .password("pass")
2260 .remote_folder("/download/folder")
2261 .local_folder(&local_folder)
2262 .passive_mode(false)
2263 .timeout(Duration::from_secs(60))
2264 .create_directories(false)
2265 .recursive(true)
2266 .build()?;
2267
2268 assert_eq!(tasklet.host, "ftp.example.com");
2269 assert_eq!(tasklet.port, 2121);
2270 assert!(!tasklet.passive_mode);
2271 assert_eq!(tasklet.timeout, Duration::from_secs(60));
2272 assert!(!tasklet.create_directories);
2273 assert!(tasklet.recursive);
2274
2275 Ok(())
2276 }
2277
2278 #[test]
2279 fn test_folder_builder_validation() {
2280 let result = FtpPutFolderTaskletBuilder::new()
2282 .username("user")
2283 .password("pass")
2284 .build();
2285 assert!(result.is_err());
2286 assert!(result
2287 .unwrap_err()
2288 .to_string()
2289 .contains("FTP host is required"));
2290
2291 let result = FtpGetFolderTaskletBuilder::new()
2293 .host("localhost")
2294 .password("pass")
2295 .build();
2296 assert!(result.is_err());
2297 assert!(result
2298 .unwrap_err()
2299 .to_string()
2300 .contains("FTP username is required"));
2301
2302 let result = FtpPutFolderTaskletBuilder::new()
2304 .host("localhost")
2305 .username("user")
2306 .password("pass")
2307 .remote_folder("/remote/folder")
2308 .build();
2309 assert!(result.is_err());
2310 assert!(result
2311 .unwrap_err()
2312 .to_string()
2313 .contains("Local folder path is required"));
2314
2315 let result = FtpGetFolderTaskletBuilder::new()
2317 .host("localhost")
2318 .username("user")
2319 .password("pass")
2320 .local_folder("/local/folder")
2321 .build();
2322 assert!(result.is_err());
2323 assert!(result
2324 .unwrap_err()
2325 .to_string()
2326 .contains("Remote folder path is required"));
2327 }
2328
2329 #[test]
2330 fn test_nonexistent_local_folder() {
2331 let result = FtpPutFolderTasklet::new(
2332 "localhost",
2333 21,
2334 "user",
2335 "pass",
2336 "/nonexistent/folder",
2337 "/remote/folder",
2338 );
2339 assert!(result.is_err());
2340 assert!(result
2341 .unwrap_err()
2342 .to_string()
2343 .contains("Local folder does not exist"));
2344 }
2345
2346 #[test]
2347 fn test_local_file_not_directory() {
2348 let temp_dir = temp_dir();
2349 let test_file = temp_dir.join("not_a_directory.txt");
2350 fs::write(&test_file, "content").unwrap();
2351
2352 let result = FtpPutFolderTasklet::new(
2353 "localhost",
2354 21,
2355 "user",
2356 "pass",
2357 &test_file,
2358 "/remote/folder",
2359 );
2360 assert!(result.is_err());
2361 assert!(result
2362 .unwrap_err()
2363 .to_string()
2364 .contains("Local path is not a directory"));
2365
2366 fs::remove_file(&test_file).ok();
2367 }
2368
2369 #[test]
2370 fn test_ftp_put_folder_tasklet_configuration() -> Result<(), BatchError> {
2371 let temp_dir = temp_dir();
2372 let test_folder = temp_dir.join("config_folder_test");
2373 fs::create_dir_all(&test_folder).unwrap();
2374 fs::write(test_folder.join("file.txt"), "content").unwrap();
2375
2376 let mut tasklet = FtpPutFolderTasklet::new(
2377 "localhost",
2378 21,
2379 "user",
2380 "pass",
2381 &test_folder,
2382 "/remote/folder",
2383 )?;
2384
2385 assert!(tasklet.passive_mode);
2387 assert_eq!(tasklet.timeout, Duration::from_secs(30));
2388 assert!(tasklet.create_directories);
2389 assert!(!tasklet.recursive);
2390
2391 tasklet.set_passive_mode(false);
2393 tasklet.set_timeout(Duration::from_secs(90));
2394 tasklet.set_create_directories(false);
2395 tasklet.set_recursive(true);
2396
2397 assert!(!tasklet.passive_mode);
2398 assert_eq!(tasklet.timeout, Duration::from_secs(90));
2399 assert!(!tasklet.create_directories);
2400 assert!(tasklet.recursive);
2401
2402 fs::remove_dir_all(&test_folder).ok();
2403 Ok(())
2404 }
2405
2406 #[test]
2407 fn test_ftp_get_folder_tasklet_configuration() -> Result<(), BatchError> {
2408 let temp_dir = temp_dir();
2409 let local_folder = temp_dir.join("config_folder_test");
2410
2411 let mut tasklet = FtpGetFolderTasklet::new(
2412 "localhost",
2413 21,
2414 "user",
2415 "pass",
2416 "/remote/folder",
2417 &local_folder,
2418 )?;
2419
2420 assert!(tasklet.passive_mode);
2422 assert_eq!(tasklet.timeout, Duration::from_secs(30));
2423 assert!(tasklet.create_directories);
2424 assert!(!tasklet.recursive);
2425
2426 tasklet.set_passive_mode(false);
2428 tasklet.set_timeout(Duration::from_secs(180));
2429 tasklet.set_create_directories(false);
2430 tasklet.set_recursive(true);
2431
2432 assert!(!tasklet.passive_mode);
2433 assert_eq!(tasklet.timeout, Duration::from_secs(180));
2434 assert!(!tasklet.create_directories);
2435 assert!(tasklet.recursive);
2436
2437 Ok(())
2438 }
2439
2440 #[test]
2441 fn test_ftp_put_folder_tasklet_execution_with_connection_error() {
2442 let temp_dir = temp_dir();
2443 let test_folder = temp_dir.join("connection_error_folder_test");
2444 fs::create_dir_all(&test_folder).unwrap();
2445 fs::write(test_folder.join("file.txt"), "content").unwrap();
2446
2447 let tasklet = FtpPutFolderTasklet::new(
2448 "nonexistent.host.invalid",
2449 21,
2450 "user",
2451 "pass",
2452 &test_folder,
2453 "/remote/folder",
2454 )
2455 .unwrap();
2456
2457 let step_execution = StepExecution::new("test-step");
2458 let result = tasklet.execute(&step_execution);
2459
2460 assert!(result.is_err());
2461 let error = result.unwrap_err();
2462 assert!(matches!(error, BatchError::Io(_)));
2463 assert!(error
2464 .to_string()
2465 .contains("Failed to connect to FTP server"));
2466
2467 fs::remove_dir_all(&test_folder).ok();
2468 }
2469
2470 #[test]
2471 fn test_ftp_get_folder_tasklet_execution_with_connection_error() {
2472 let temp_dir = temp_dir();
2473 let local_folder = temp_dir.join("connection_error_folder_test");
2474
2475 let tasklet = FtpGetFolderTasklet::new(
2476 "nonexistent.host.invalid",
2477 21,
2478 "user",
2479 "pass",
2480 "/remote/folder",
2481 &local_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!(error
2492 .to_string()
2493 .contains("Failed to connect to FTP server"));
2494 }
2495
2496 #[test]
2497 fn test_builder_default_implementations() {
2498 let _put_builder = FtpPutTaskletBuilder::default();
2500 let _get_builder = FtpGetTaskletBuilder::default();
2501 let _put_folder_builder = FtpPutFolderTaskletBuilder::default();
2502 let _get_folder_builder = FtpGetFolderTaskletBuilder::default();
2503 }
2504
2505 #[test]
2506 fn test_builder_fluent_interface() -> Result<(), BatchError> {
2507 let temp_dir = temp_dir();
2508 let test_file = temp_dir.join("fluent_test.txt");
2509 fs::write(&test_file, "test content").unwrap();
2510
2511 let tasklet = FtpPutTaskletBuilder::new()
2513 .host("example.com")
2514 .port(2121)
2515 .username("testuser")
2516 .password("testpass")
2517 .local_file(&test_file)
2518 .remote_file("/remote/test.txt")
2519 .passive_mode(true)
2520 .timeout(Duration::from_secs(45))
2521 .build()?;
2522
2523 assert_eq!(tasklet.host, "example.com");
2524 assert_eq!(tasklet.port, 2121);
2525 assert_eq!(tasklet.username, "testuser");
2526 assert_eq!(tasklet.password, "testpass");
2527 assert_eq!(tasklet.remote_file, "/remote/test.txt");
2528 assert!(tasklet.passive_mode);
2529 assert_eq!(tasklet.timeout, Duration::from_secs(45));
2530
2531 fs::remove_file(&test_file).ok();
2532 Ok(())
2533 }
2534
2535 #[test]
2536 fn test_error_message_quality() {
2537 let result = FtpPutTaskletBuilder::new().build();
2539 assert!(result.is_err());
2540 let error_msg = result.unwrap_err().to_string();
2541 assert!(error_msg.contains("FTP host is required"));
2542
2543 let result = FtpPutTaskletBuilder::new().host("localhost").build();
2544 assert!(result.is_err());
2545 let error_msg = result.unwrap_err().to_string();
2546 assert!(error_msg.contains("FTP username is required"));
2547 }
2548
2549 #[test]
2550 fn test_path_handling() -> Result<(), BatchError> {
2551 let temp_dir = temp_dir();
2552 let test_file = temp_dir.join("path_test.txt");
2553 fs::write(&test_file, "test content").unwrap();
2554
2555 let tasklet1 = FtpPutTasklet::new(
2557 "localhost",
2558 21,
2559 "user",
2560 "pass",
2561 &test_file,
2562 "/remote/file.txt",
2563 )?;
2564
2565 let tasklet2 = FtpPutTasklet::new(
2566 "localhost",
2567 21,
2568 "user",
2569 "pass",
2570 test_file.as_path(),
2571 "/remote/file.txt",
2572 )?;
2573
2574 assert_eq!(tasklet1.local_file, tasklet2.local_file);
2575
2576 fs::remove_file(&test_file).ok();
2577 Ok(())
2578 }
2579
2580 #[test]
2581 fn test_timeout_configuration() -> Result<(), BatchError> {
2582 let temp_dir = temp_dir();
2583 let test_file = temp_dir.join("timeout_test.txt");
2584 fs::write(&test_file, "test content").unwrap();
2585
2586 let tasklet = FtpPutTaskletBuilder::new()
2588 .host("localhost")
2589 .username("user")
2590 .password("pass")
2591 .local_file(&test_file)
2592 .remote_file("/remote/file.txt")
2593 .timeout(Duration::from_millis(500))
2594 .build()?;
2595
2596 assert_eq!(tasklet.timeout, Duration::from_millis(500));
2597
2598 let tasklet = FtpPutTaskletBuilder::new()
2599 .host("localhost")
2600 .username("user")
2601 .password("pass")
2602 .local_file(&test_file)
2603 .remote_file("/remote/file.txt")
2604 .timeout(Duration::from_secs(300))
2605 .build()?;
2606
2607 assert_eq!(tasklet.timeout, Duration::from_secs(300));
2608
2609 fs::remove_file(&test_file).ok();
2610 Ok(())
2611 }
2612
2613 #[test]
2614 fn test_port_configuration() -> Result<(), BatchError> {
2615 let temp_dir = temp_dir();
2616 let test_file = temp_dir.join("port_test.txt");
2617 fs::write(&test_file, "test content").unwrap();
2618
2619 let tasklet = FtpPutTaskletBuilder::new()
2621 .host("localhost")
2622 .port(990) .username("user")
2624 .password("pass")
2625 .local_file(&test_file)
2626 .remote_file("/remote/file.txt")
2627 .build()?;
2628
2629 assert_eq!(tasklet.port, 990);
2630
2631 let tasklet = FtpPutTaskletBuilder::new()
2632 .host("localhost")
2633 .port(2121) .username("user")
2635 .password("pass")
2636 .local_file(&test_file)
2637 .remote_file("/remote/file.txt")
2638 .build()?;
2639
2640 assert_eq!(tasklet.port, 2121);
2641
2642 fs::remove_file(&test_file).ok();
2643 Ok(())
2644 }
2645
2646 #[test]
2647 fn test_passive_mode_configuration() -> Result<(), BatchError> {
2648 let temp_dir = temp_dir();
2649 let test_file = temp_dir.join("passive_test.txt");
2650 fs::write(&test_file, "test content").unwrap();
2651
2652 let tasklet = FtpPutTaskletBuilder::new()
2654 .host("localhost")
2655 .username("user")
2656 .password("pass")
2657 .local_file(&test_file)
2658 .remote_file("/remote/file.txt")
2659 .passive_mode(true)
2660 .build()?;
2661
2662 assert!(tasklet.passive_mode);
2663
2664 let tasklet = FtpPutTaskletBuilder::new()
2666 .host("localhost")
2667 .username("user")
2668 .password("pass")
2669 .local_file(&test_file)
2670 .remote_file("/remote/file.txt")
2671 .passive_mode(false)
2672 .build()?;
2673
2674 assert!(!tasklet.passive_mode);
2675
2676 fs::remove_file(&test_file).ok();
2677 Ok(())
2678 }
2679
2680 #[test]
2681 fn test_secure_ftp_configuration() -> Result<(), BatchError> {
2682 let temp_dir = temp_dir();
2683 let test_file = temp_dir.join("secure_test.txt");
2684 fs::write(&test_file, "test content").unwrap();
2685
2686 let tasklet = FtpPutTaskletBuilder::new()
2688 .host("localhost")
2689 .username("user")
2690 .password("pass")
2691 .local_file(&test_file)
2692 .remote_file("/remote/file.txt")
2693 .build()?;
2694
2695 assert!(!tasklet.secure);
2696
2697 let tasklet = FtpPutTaskletBuilder::new()
2699 .host("secure-ftp.example.com")
2700 .port(990)
2701 .username("user")
2702 .password("pass")
2703 .local_file(&test_file)
2704 .remote_file("/secure/file.txt")
2705 .secure(true)
2706 .build()?;
2707
2708 assert!(tasklet.secure);
2709 assert_eq!(tasklet.port, 990);
2710
2711 let local_file = temp_dir.join("downloaded_secure.txt");
2713 let get_tasklet = FtpGetTaskletBuilder::new()
2714 .host("secure-ftp.example.com")
2715 .port(990)
2716 .username("user")
2717 .password("pass")
2718 .remote_file("/secure/file.txt")
2719 .local_file(&local_file)
2720 .secure(true)
2721 .build()?;
2722
2723 assert!(get_tasklet.secure);
2724 assert_eq!(get_tasklet.port, 990);
2725
2726 fs::remove_file(&test_file).ok();
2727 Ok(())
2728 }
2729
2730 #[test]
2731 fn test_secure_ftp_folder_configuration() -> Result<(), BatchError> {
2732 let temp_dir = temp_dir();
2733 let test_folder = temp_dir.join("secure_folder_test");
2734 fs::create_dir_all(&test_folder).unwrap();
2735 fs::write(test_folder.join("file.txt"), "test content").unwrap();
2736
2737 let tasklet = FtpPutFolderTaskletBuilder::new()
2739 .host("localhost")
2740 .username("user")
2741 .password("pass")
2742 .local_folder(&test_folder)
2743 .remote_folder("/remote/folder")
2744 .build()?;
2745
2746 assert!(!tasklet.secure);
2747
2748 let tasklet = FtpPutFolderTaskletBuilder::new()
2750 .host("secure-ftp.example.com")
2751 .port(990)
2752 .username("user")
2753 .password("pass")
2754 .local_folder(&test_folder)
2755 .remote_folder("/secure/folder")
2756 .secure(true)
2757 .build()?;
2758
2759 assert!(tasklet.secure);
2760 assert_eq!(tasklet.port, 990);
2761
2762 let local_folder = temp_dir.join("downloaded_secure_folder");
2764 let get_tasklet = FtpGetFolderTaskletBuilder::new()
2765 .host("secure-ftp.example.com")
2766 .port(990)
2767 .username("user")
2768 .password("pass")
2769 .remote_folder("/secure/folder")
2770 .local_folder(&local_folder)
2771 .secure(true)
2772 .build()?;
2773
2774 assert!(get_tasklet.secure);
2775 assert_eq!(get_tasklet.port, 990);
2776
2777 fs::remove_dir_all(&test_folder).ok();
2778 Ok(())
2779 }
2780}