//! ## Ftp_transfer //! //! `ftp_transfer` is the module which provides the implementation for the FTP/FTPS file transfer /** * MIT License * * termscp - Copyright (c) 2021 Christian Visintin * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ use super::{FileTransfer, FileTransferError, FileTransferErrorType}; use crate::fs::{FsDirectory, FsEntry, FsFile}; use crate::utils::fmt::shadow_password; // Includes use std::convert::TryFrom; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; use std::time::UNIX_EPOCH; use suppaftp::native_tls::TlsConnector; use suppaftp::{ list::{File, PosixPexQuery}, status::FILE_UNAVAILABLE, types::{FileType, Response}, FtpError, FtpStream, }; /// ## FtpFileTransfer /// /// Ftp file transfer struct pub struct FtpFileTransfer { stream: Option, ftps: bool, } impl FtpFileTransfer { /// ### new /// /// Instantiates a new `FtpFileTransfer` pub fn new(ftps: bool) -> FtpFileTransfer { FtpFileTransfer { stream: None, ftps } } /// ### resolve /// /// Fix provided path; on Windows fixes the backslashes, converting them to slashes /// While on POSIX does nothing #[cfg(target_os = "windows")] fn resolve(p: &Path) -> PathBuf { PathBuf::from(path_slash::PathExt::to_slash_lossy(p).as_str()) } #[cfg(target_family = "unix")] fn resolve(p: &Path) -> PathBuf { p.to_path_buf() } /// ### parse_list_lines /// /// Parse all lines of LIST command output and instantiates a vector of FsEntry from it. /// This function also converts from `suppaftp::list::File` to `FsEntry` fn parse_list_lines(&mut self, path: &Path, lines: Vec) -> Vec { // Iter and collect lines .into_iter() .map(File::try_from) // Try to convert to file .flatten() // Remove errors .map(|x| { let mut abs_path: PathBuf = path.to_path_buf(); abs_path.push(x.name()); match x.is_directory() { true => FsEntry::Directory(FsDirectory { name: x.name().to_string(), abs_path, last_access_time: x.modified(), last_change_time: x.modified(), creation_time: x.modified(), readonly: false, symlink: None, user: x.uid(), group: x.gid(), unix_pex: Some(Self::query_unix_pex(&x)), }), false => FsEntry::File(FsFile { name: x.name().to_string(), size: x.size(), ftype: abs_path .extension() .map(|ext| String::from(ext.to_str().unwrap_or(""))), last_access_time: x.modified(), last_change_time: x.modified(), creation_time: x.modified(), readonly: false, user: x.uid(), group: x.gid(), symlink: Self::get_symlink_entry(path, x.symlink()), abs_path, unix_pex: Some(Self::query_unix_pex(&x)), }), } }) .collect() } /// ### get_symlink_entry /// /// Get FsEntry from symlink fn get_symlink_entry(wrkdir: &Path, link: Option<&Path>) -> Option> { match link { None => None, Some(p) => { // Make abs path let abs_path: PathBuf = match p.is_absolute() { true => p.to_path_buf(), false => { let mut abs = wrkdir.to_path_buf(); abs.push(p); abs } }; Some(Box::new(FsEntry::File(FsFile { name: p .file_name() .map(|x| x.to_str().unwrap_or("").to_string()) .unwrap_or_default(), ftype: abs_path .extension() .map(|ext| String::from(ext.to_str().unwrap_or(""))), size: 0, last_access_time: UNIX_EPOCH, last_change_time: UNIX_EPOCH, creation_time: UNIX_EPOCH, user: None, group: None, readonly: false, symlink: None, unix_pex: None, abs_path, }))) } } } /// ### query_unix_pex /// /// Returns unix pex in tuple of values fn query_unix_pex(f: &File) -> (u8, u8, u8) { ( Self::pex_to_byte( f.can_read(PosixPexQuery::Owner), f.can_write(PosixPexQuery::Owner), f.can_execute(PosixPexQuery::Owner), ), Self::pex_to_byte( f.can_read(PosixPexQuery::Group), f.can_write(PosixPexQuery::Group), f.can_execute(PosixPexQuery::Group), ), Self::pex_to_byte( f.can_read(PosixPexQuery::Others), f.can_write(PosixPexQuery::Others), f.can_execute(PosixPexQuery::Others), ), ) } /// ### pex_to_byte /// /// Convert unix permissions to byte value fn pex_to_byte(read: bool, write: bool, exec: bool) -> u8 { ((read as u8) << 2) + ((write as u8) << 1) + (exec as u8) } } impl FileTransfer for FtpFileTransfer { /// ### connect /// /// Connect to the remote server fn connect( &mut self, address: String, port: u16, username: Option, password: Option, ) -> Result, FileTransferError> { // Get stream info!("Connecting to {}:{}", address, port); let mut stream: FtpStream = match FtpStream::connect(format!("{}:{}", address, port)) { Ok(stream) => stream, Err(err) => { error!("Failed to connect: {}", err); return Err(FileTransferError::new_ex( FileTransferErrorType::ConnectionError, err.to_string(), )); } }; // If SSL, open secure session if self.ftps { info!("Setting up TLS stream..."); let ctx = match TlsConnector::builder() .danger_accept_invalid_certs(true) .danger_accept_invalid_hostnames(true) .build() { Ok(tls) => tls, Err(err) => { error!("Failed to setup TLS stream: {}", err); return Err(FileTransferError::new_ex( FileTransferErrorType::SslError, err.to_string(), )); } }; stream = match stream.into_secure(ctx, address.as_str()) { Ok(s) => s, Err(err) => { error!("Failed to setup TLS stream: {}", err); return Err(FileTransferError::new_ex( FileTransferErrorType::SslError, err.to_string(), )); } }; } // Login (use anonymous if credentials are unspecified) let username: String = match username { Some(u) => u, None => String::from("anonymous"), }; let password: String = match password { Some(pwd) => pwd, None => String::new(), }; info!( "Signin in with username: {}, password: {}", username, shadow_password(password.as_str()) ); if let Err(err) = stream.login(username.as_str(), password.as_str()) { error!("Login failed: {}", err); return Err(FileTransferError::new_ex( FileTransferErrorType::AuthenticationFailed, err.to_string(), )); } debug!("Setting transfer type to Binary"); // Initialize file type if let Err(err) = stream.transfer_type(FileType::Binary) { error!("Failed to set transfer type to binary: {}", err); return Err(FileTransferError::new_ex( FileTransferErrorType::ProtocolError, err.to_string(), )); } // Set stream self.stream = Some(stream); info!("Connection successfully established"); // Return OK Ok(self .stream .as_ref() .unwrap() .get_welcome_msg() .map(|x| x.to_string())) } /// ### disconnect /// /// Disconnect from the remote server fn disconnect(&mut self) -> Result<(), FileTransferError> { info!("Disconnecting from FTP server..."); match &mut self.stream { Some(stream) => match stream.quit() { Ok(_) => { self.stream = None; Ok(()) } Err(err) => Err(FileTransferError::new_ex( FileTransferErrorType::ConnectionError, err.to_string(), )), }, None => Err(FileTransferError::new( FileTransferErrorType::UninitializedSession, )), } } /// ### is_connected /// /// Indicates whether the client is connected to remote fn is_connected(&self) -> bool { self.stream.is_some() } /// ### pwd /// /// Print working directory fn pwd(&mut self) -> Result { info!("PWD"); match &mut self.stream { Some(stream) => match stream.pwd() { Ok(path) => Ok(PathBuf::from(path.as_str())), Err(err) => Err(FileTransferError::new_ex( FileTransferErrorType::ConnectionError, err.to_string(), )), }, None => Err(FileTransferError::new( FileTransferErrorType::UninitializedSession, )), } } /// ### change_dir /// /// Change working directory fn change_dir(&mut self, dir: &Path) -> Result { let dir: PathBuf = Self::resolve(dir); info!("Changing directory to {}", dir.display()); match &mut self.stream { Some(stream) => match stream.cwd(&dir.as_path().to_string_lossy()) { Ok(_) => Ok(dir), Err(err) => Err(FileTransferError::new_ex( FileTransferErrorType::ConnectionError, err.to_string(), )), }, None => Err(FileTransferError::new( FileTransferErrorType::UninitializedSession, )), } } /// ### copy /// /// Copy file to destination fn copy(&mut self, _src: &FsEntry, _dst: &Path) -> Result<(), FileTransferError> { // FTP doesn't support file copy debug!("COPY issues (will fail, since unsupported)"); Err(FileTransferError::new( FileTransferErrorType::UnsupportedFeature, )) } /// ### list_dir /// /// List directory entries fn list_dir(&mut self, path: &Path) -> Result, FileTransferError> { let dir: PathBuf = Self::resolve(path); info!("LIST dir {}", dir.display()); match &mut self.stream { Some(stream) => match stream.list(Some(&dir.as_path().to_string_lossy())) { Ok(lines) => { debug!("Got {} lines in LIST result", lines.len()); // Iterate over entries Ok(self.parse_list_lines(path, lines)) } Err(err) => Err(FileTransferError::new_ex( FileTransferErrorType::DirStatFailed, err.to_string(), )), }, None => Err(FileTransferError::new( FileTransferErrorType::UninitializedSession, )), } } /// ### mkdir /// /// In case the directory already exists, it must return an Error of kind `FileTransferErrorType::DirectoryAlreadyExists` fn mkdir(&mut self, dir: &Path) -> Result<(), FileTransferError> { let dir: PathBuf = Self::resolve(dir); info!("MKDIR {}", dir.display()); match &mut self.stream { Some(stream) => match stream.mkdir(&dir.as_path().to_string_lossy()) { Ok(_) => Ok(()), Err(FtpError::UnexpectedResponse(Response { // Directory already exists code: FILE_UNAVAILABLE, body: _, })) => { error!("Directory {} already exists", dir.display()); Err(FileTransferError::new( FileTransferErrorType::DirectoryAlreadyExists, )) } Err(err) => Err(FileTransferError::new_ex( FileTransferErrorType::FileCreateDenied, err.to_string(), )), }, None => Err(FileTransferError::new( FileTransferErrorType::UninitializedSession, )), } } /// ### remove /// /// Remove a file or a directory fn remove(&mut self, fsentry: &FsEntry) -> Result<(), FileTransferError> { if self.stream.is_none() { return Err(FileTransferError::new( FileTransferErrorType::UninitializedSession, )); } info!("Removing entry {}", fsentry.get_abs_path().display()); let wrkdir: PathBuf = self.pwd()?; match fsentry { // Match fs entry... FsEntry::File(file) => { // Go to parent directory if let Some(parent_dir) = file.abs_path.parent() { debug!("Changing wrkdir to {}", parent_dir.display()); self.change_dir(parent_dir)?; } debug!("entry is a file; removing file {}", file.abs_path.display()); // Remove file directly let result = self .stream .as_mut() .unwrap() .rm(file.name.as_ref()) .map(|_| ()) .map_err(|e| { FileTransferError::new_ex(FileTransferErrorType::PexError, e.to_string()) }); // Go to source directory match self.change_dir(wrkdir.as_path()) { Err(err) => Err(err), Ok(_) => result, } } FsEntry::Directory(dir) => { // Get directory files debug!("Entry is a directory; iterating directory entries"); let result = match self.list_dir(dir.abs_path.as_path()) { Ok(files) => { // Remove recursively files debug!("Removing {} entries from directory...", files.len()); for file in files.iter() { if let Err(err) = self.remove(file) { return Err(FileTransferError::new_ex( FileTransferErrorType::PexError, err.to_string(), )); } } // Once all files in directory have been deleted, remove directory debug!("Finally removing directory {}...", dir.name); // Enter parent directory if let Some(parent_dir) = dir.abs_path.parent() { debug!( "Changing wrkdir to {} to delete directory {}", parent_dir.display(), dir.name ); self.change_dir(parent_dir)?; } match self.stream.as_mut().unwrap().rmdir(dir.name.as_str()) { Ok(_) => { debug!("Removed {}", dir.abs_path.display()); Ok(()) } Err(err) => Err(FileTransferError::new_ex( FileTransferErrorType::PexError, err.to_string(), )), } } Err(err) => Err(FileTransferError::new_ex( FileTransferErrorType::DirStatFailed, err.to_string(), )), }; // Restore directory match self.change_dir(wrkdir.as_path()) { Err(err) => Err(err), Ok(_) => result, } } } } /// ### rename /// /// Rename file or a directory fn rename(&mut self, file: &FsEntry, dst: &Path) -> Result<(), FileTransferError> { let dst: PathBuf = Self::resolve(dst); info!( "Renaming {} to {}", file.get_abs_path().display(), dst.display() ); match &mut self.stream { Some(stream) => { // Get name let src_name: String = match file { FsEntry::Directory(dir) => dir.name.clone(), FsEntry::File(file) => file.name.clone(), }; // Only names are supported match stream.rename(src_name.as_str(), &dst.as_path().to_string_lossy()) { Ok(_) => Ok(()), Err(err) => Err(FileTransferError::new_ex( FileTransferErrorType::FileCreateDenied, err.to_string(), )), } } None => Err(FileTransferError::new( FileTransferErrorType::UninitializedSession, )), } } /// ### stat /// /// Stat file and return FsEntry fn stat(&mut self, _path: &Path) -> Result { match &mut self.stream { Some(_) => Err(FileTransferError::new( FileTransferErrorType::UnsupportedFeature, )), None => Err(FileTransferError::new( FileTransferErrorType::UninitializedSession, )), } } /// ### exec /// /// Execute a command on remote host fn exec(&mut self, _cmd: &str) -> Result { Err(FileTransferError::new( FileTransferErrorType::UnsupportedFeature, )) } /// ### send_file /// /// Send file to remote /// File name is referred to the name of the file as it will be saved /// Data contains the file data /// Returns file and its size fn send_file( &mut self, _local: &FsFile, file_name: &Path, ) -> Result, FileTransferError> { let file_name: PathBuf = Self::resolve(file_name); info!("Sending file {}", file_name.display()); match &mut self.stream { Some(stream) => match stream.put_with_stream(&file_name.as_path().to_string_lossy()) { Ok(writer) => Ok(Box::new(writer)), // NOTE: don't use BufWriter here, since already returned by the library Err(err) => Err(FileTransferError::new_ex( FileTransferErrorType::FileCreateDenied, err.to_string(), )), }, None => Err(FileTransferError::new( FileTransferErrorType::UninitializedSession, )), } } /// ### recv_file /// /// Receive file from remote with provided name /// Returns file and its size fn recv_file(&mut self, file: &FsFile) -> Result, FileTransferError> { info!("Receiving file {}", file.abs_path.display()); match &mut self.stream { Some(stream) => match stream.retr_as_stream(&file.abs_path.as_path().to_string_lossy()) { Ok(reader) => Ok(Box::new(reader)), // NOTE: don't use BufReader here, since already returned by the library Err(err) => Err(FileTransferError::new_ex( FileTransferErrorType::NoSuchFileOrDirectory, err.to_string(), )), }, None => Err(FileTransferError::new( FileTransferErrorType::UninitializedSession, )), } } /// ### on_sent /// /// Finalize send method. /// This method must be implemented only if necessary; in case you don't need it, just return `Ok(())` /// The purpose of this method is to finalize the connection with the peer when writing data. /// This is necessary for some protocols such as FTP. /// You must call this method each time you want to finalize the write of the remote file. fn on_sent(&mut self, writable: Box) -> Result<(), FileTransferError> { info!("Finalizing put stream"); match &mut self.stream { Some(stream) => match stream.finalize_put_stream(writable) { Ok(_) => Ok(()), Err(err) => Err(FileTransferError::new_ex( FileTransferErrorType::ProtocolError, err.to_string(), )), }, None => Err(FileTransferError::new( FileTransferErrorType::UninitializedSession, )), } } /// ### on_recv /// /// Finalize recv method. /// This method must be implemented only if necessary; in case you don't need it, just return `Ok(())` /// The purpose of this method is to finalize the connection with the peer when reading data. /// This mighe be necessary for some protocols. /// You must call this method each time you want to finalize the read of the remote file. fn on_recv(&mut self, readable: Box) -> Result<(), FileTransferError> { info!("Finalizing get"); match &mut self.stream { Some(stream) => match stream.finalize_retr_stream(readable) { Ok(_) => Ok(()), Err(err) => Err(FileTransferError::new_ex( FileTransferErrorType::ProtocolError, err.to_string(), )), }, None => Err(FileTransferError::new( FileTransferErrorType::UninitializedSession, )), } } } #[cfg(test)] mod tests { use super::*; use crate::utils::file::open_file; #[cfg(feature = "with-containers")] use crate::utils::test_helpers::write_file; use crate::utils::test_helpers::{create_sample_file_entry, make_fsentry}; use pretty_assertions::assert_eq; use std::io::{Read, Write}; use std::time::Duration; #[test] fn test_filetransfer_ftp_new() { let ftp: FtpFileTransfer = FtpFileTransfer::new(false); assert_eq!(ftp.ftps, false); assert!(ftp.stream.is_none()); // FTPS let ftp: FtpFileTransfer = FtpFileTransfer::new(true); assert_eq!(ftp.ftps, true); assert!(ftp.stream.is_none()); } #[test] #[cfg(feature = "with-containers")] fn test_filetransfer_ftp_server() { let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false); // Sample file let (entry, file): (FsFile, tempfile::NamedTempFile) = create_sample_file_entry(); // Connect #[cfg(not(feature = "github-actions"))] let hostname: String = String::from("127.0.0.1"); #[cfg(feature = "github-actions")] let hostname: String = String::from("127.0.0.1"); assert!(ftp .connect( hostname, 10021, Some(String::from("test")), Some(String::from("test")), ) .is_ok()); assert_eq!(ftp.is_connected(), true); // Get pwd assert_eq!(ftp.pwd().unwrap(), PathBuf::from("/")); // List dir (dir is empty) assert_eq!(ftp.list_dir(&Path::new("/")).unwrap().len(), 0); // Make directory assert!(ftp.mkdir(PathBuf::from("/home").as_path()).is_ok()); // Remake directory (should report already exists) assert_eq!( ftp.mkdir(PathBuf::from("/home").as_path()) .err() .unwrap() .kind(), FileTransferErrorType::DirectoryAlreadyExists ); // Make directory (err) assert!(ftp.mkdir(PathBuf::from("/root/pommlar").as_path()).is_err()); // Change directory assert!(ftp.change_dir(PathBuf::from("/home").as_path()).is_ok()); // Change directory (err) assert!(ftp .change_dir(PathBuf::from("/tmp/oooo/aaaa/eee").as_path()) .is_err()); // Copy (not supported) assert!(ftp .copy(&FsEntry::File(entry.clone()), PathBuf::from("/").as_path()) .is_err()); // Exec (not supported) assert!(ftp.exec("echo 1;").is_err()); // Upload 2 files let mut writable = ftp .send_file(&entry, PathBuf::from("omar.txt").as_path()) .ok() .unwrap(); write_file(&file, &mut writable); assert!(ftp.on_sent(writable).is_ok()); let mut writable = ftp .send_file(&entry, PathBuf::from("README.md").as_path()) .ok() .unwrap(); write_file(&file, &mut writable); assert!(ftp.on_sent(writable).is_ok()); // Upload file (err) assert!(ftp .send_file(&entry, PathBuf::from("/ommlar/omarone").as_path()) .is_err()); // List dir let list: Vec = ftp.list_dir(PathBuf::from("/home").as_path()).ok().unwrap(); assert_eq!(list.len(), 2); // Find assert!(ftp.change_dir(PathBuf::from("/").as_path()).is_ok()); assert_eq!(ftp.find("*.txt").ok().unwrap().len(), 1); assert_eq!(ftp.find("*.md").ok().unwrap().len(), 1); assert_eq!(ftp.find("*.jpeg").ok().unwrap().len(), 0); assert!(ftp.change_dir(PathBuf::from("/home").as_path()).is_ok()); // Rename assert!(ftp.mkdir(PathBuf::from("/uploads").as_path()).is_ok()); assert!(ftp .rename( list.get(0).unwrap(), PathBuf::from("/uploads/README.txt").as_path() ) .is_ok()); // Rename (err) assert!(ftp .rename(list.get(0).unwrap(), PathBuf::from("OMARONE").as_path()) .is_err()); let dummy: FsEntry = FsEntry::File(FsFile { name: String::from("cucumber.txt"), abs_path: PathBuf::from("/cucumber.txt"), last_change_time: UNIX_EPOCH, last_access_time: UNIX_EPOCH, creation_time: UNIX_EPOCH, size: 0, ftype: Some(String::from("txt")), // File type readonly: true, symlink: None, // UNIX only user: Some(0), // UNIX only group: Some(0), // UNIX only unix_pex: Some((6, 4, 4)), // UNIX only }); assert!(ftp .rename(&dummy, PathBuf::from("/a/b/c").as_path()) .is_err()); // Remove assert!(ftp.remove(list.get(1).unwrap()).is_ok()); assert!(ftp.remove(list.get(1).unwrap()).is_err()); // Receive file let mut writable = ftp .send_file(&entry, PathBuf::from("/uploads/README.txt").as_path()) .ok() .unwrap(); write_file(&file, &mut writable); assert!(ftp.on_sent(writable).is_ok()); let file: FsFile = ftp .list_dir(PathBuf::from("/uploads").as_path()) .ok() .unwrap() .get(0) .unwrap() .clone() .unwrap_file(); let mut readable = ftp.recv_file(&file).ok().unwrap(); let mut data: Vec = vec![0; 1024]; assert!(readable.read(&mut data).is_ok()); assert!(ftp.on_recv(readable).is_ok()); // Receive file (err) assert!(ftp.recv_file(&entry).is_err()); // Cleanup assert!(ftp.change_dir(PathBuf::from("/").as_path()).is_ok()); assert!(ftp .remove(&make_fsentry(PathBuf::from("/home"), true)) .is_ok()); assert!(ftp .remove(&make_fsentry(PathBuf::from("/uploads"), true)) .is_ok()); // Disconnect assert!(ftp.disconnect().is_ok()); assert_eq!(ftp.is_connected(), false); } #[test] #[cfg(feature = "with-containers")] fn test_filetransfer_ftp_server_bad_auth() { let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false); // Connect assert!(ftp .connect( String::from("127.0.0.1"), 10021, Some(String::from("omar")), Some(String::from("ommlar")), ) .is_err()); } #[test] #[cfg(feature = "with-containers")] fn test_filetransfer_ftp_no_credentials() { let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false); assert!(ftp .connect(String::from("127.0.0.1"), 10021, None, None) .is_err()); } #[test] fn test_filetransfer_ftp_server_bad_server() { let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false); // Connect assert!(ftp .connect( String::from("mybadserver.veryverybad.awful"), 21, Some(String::from("omar")), Some(String::from("ommlar")), ) .is_err()); } #[test] fn test_filetransfer_ftp_parse_list_line_unix() { let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false); // Simple file let file: FsFile = ftp .parse_list_lines( PathBuf::from("/tmp").as_path(), vec!["-rw-rw-r-- 1 root dialout 8192 Nov 5 2018 omar.txt".to_string()], ) .get(0) .unwrap() .clone() .unwrap_file(); assert_eq!(file.abs_path, PathBuf::from("/tmp/omar.txt")); assert_eq!(file.name, String::from("omar.txt")); assert_eq!(file.size, 8192); assert!(file.symlink.is_none()); assert_eq!(file.user, None); assert_eq!(file.group, None); assert_eq!(file.unix_pex.unwrap(), (6, 6, 4)); assert_eq!( file.last_access_time .duration_since(UNIX_EPOCH) .ok() .unwrap(), Duration::from_secs(1541376000) ); assert_eq!( file.last_change_time .duration_since(UNIX_EPOCH) .ok() .unwrap(), Duration::from_secs(1541376000) ); assert_eq!( file.creation_time.duration_since(UNIX_EPOCH).ok().unwrap(), Duration::from_secs(1541376000) ); } #[test] fn test_filetransfer_ftp_list_dir_dos_syntax() { let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false); // Connect assert!(ftp .connect( String::from("test.rebex.net"), 21, Some(String::from("demo")), Some(String::from("password")) ) .is_ok()); // Pwd assert_eq!(ftp.pwd().ok().unwrap(), PathBuf::from("/")); // List dir let files: Vec = ftp.list_dir(PathBuf::from("/").as_path()).ok().unwrap(); // There should be at least 1 file assert!(files.len() > 0); // Disconnect assert!(ftp.disconnect().is_ok()); } #[test] fn test_filetransfer_ftp_uninitialized() { let file: FsFile = FsFile { name: String::from("omar.txt"), abs_path: PathBuf::from("/omar.txt"), last_change_time: UNIX_EPOCH, last_access_time: UNIX_EPOCH, creation_time: UNIX_EPOCH, size: 0, ftype: Some(String::from("txt")), // File type readonly: true, symlink: None, // UNIX only user: Some(0), // UNIX only group: Some(0), // UNIX only unix_pex: Some((6, 4, 4)), // UNIX only }; let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false); assert!(ftp.change_dir(Path::new("/tmp")).is_err()); assert!(ftp.disconnect().is_err()); assert!(ftp.list_dir(Path::new("/tmp")).is_err()); assert!(ftp.mkdir(Path::new("/tmp")).is_err()); assert!(ftp .remove(&make_fsentry(PathBuf::from("/nowhere"), false)) .is_err()); assert!(ftp .rename( &make_fsentry(PathBuf::from("/nowhere"), false), PathBuf::from("/culonia").as_path() ) .is_err()); assert!(ftp.pwd().is_err()); assert!(ftp.stat(Path::new("/tmp")).is_err()); assert!(ftp.recv_file(&file).is_err()); assert!(ftp.send_file(&file, Path::new("/tmp/omar.txt")).is_err()); let (_, temp): (FsFile, tempfile::NamedTempFile) = create_sample_file_entry(); let readable: Box = Box::new(std::fs::File::open(temp.path()).unwrap()); assert!(ftp.on_recv(readable).is_err()); let (_, temp): (FsFile, tempfile::NamedTempFile) = create_sample_file_entry(); let writable: Box = Box::new(open_file(temp.path(), true, true, true).ok().unwrap()); assert!(ftp.on_sent(writable).is_err()); } }