From 08728bf55ef4354d1de555b5df5d3c06fb85e250 Mon Sep 17 00:00:00 2001 From: ChristianVisintin Date: Mon, 21 Dec 2020 10:49:31 +0100 Subject: [PATCH] Copy method (host/transfer) --- src/filetransfer/ftp_transfer.rs | 39 +++++++ src/filetransfer/mod.rs | 5 + src/filetransfer/scp_transfer.rs | 41 +++++++ src/filetransfer/sftp_transfer.rs | 45 ++++++++ src/host/mod.rs | 180 +++++++++++++++++++++++++++--- 5 files changed, 292 insertions(+), 18 deletions(-) diff --git a/src/filetransfer/ftp_transfer.rs b/src/filetransfer/ftp_transfer.rs index 643adbe..d699a9a 100644 --- a/src/filetransfer/ftp_transfer.rs +++ b/src/filetransfer/ftp_transfer.rs @@ -344,6 +344,16 @@ impl FileTransfer for FtpFileTransfer { } } + /// ### copy + /// + /// Copy file to destination + fn copy(&mut self, _src: &FsEntry, _dst: &Path) -> Result<(), FileTransferError> { + // FTP doesn't support file copy + Err(FileTransferError::new( + FileTransferErrorType::UnsupportedFeature, + )) + } + /// ### list_dir /// /// List directory entries @@ -796,6 +806,35 @@ mod tests { assert!(ftp.disconnect().is_ok()); } + #[test] + fn test_filetransfer_sftp_copy() { + let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false); + // Connect + assert!(ftp + .connect(String::from("speedtest.tele2.net"), 21, None, None) + .is_ok()); + // Pwd + assert_eq!(ftp.pwd().ok().unwrap(), PathBuf::from("/")); + // Copy + let file: FsFile = FsFile { + name: String::from("readme.txt"), + abs_path: PathBuf::from("/readme.txt"), + last_change_time: SystemTime::UNIX_EPOCH, + last_access_time: SystemTime::UNIX_EPOCH, + creation_time: SystemTime::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 + .copy(&FsEntry::File(file), &Path::new("/tmp/dest.txt")) + .is_err()); + } + /* NOTE: they don't work #[test] fn test_filetransfer_ftp_list_dir() { diff --git a/src/filetransfer/mod.rs b/src/filetransfer/mod.rs index 90325b2..243573c 100644 --- a/src/filetransfer/mod.rs +++ b/src/filetransfer/mod.rs @@ -157,6 +157,11 @@ pub trait FileTransfer { fn change_dir(&mut self, dir: &Path) -> Result; + /// ### copy + /// + /// Copy file to destination + fn copy(&mut self, src: &FsEntry, dst: &Path) -> Result<(), FileTransferError>; + /// ### list_dir /// /// List directory entries diff --git a/src/filetransfer/scp_transfer.rs b/src/filetransfer/scp_transfer.rs index 2843f39..b7a1deb 100644 --- a/src/filetransfer/scp_transfer.rs +++ b/src/filetransfer/scp_transfer.rs @@ -468,6 +468,47 @@ impl FileTransfer for ScpFileTransfer { } } + /// ### copy + /// + /// Copy file to destination + fn copy(&mut self, src: &FsEntry, dst: &Path) -> Result<(), FileTransferError> { + match self.is_connected() { + true => { + // Run `cp -rf` + let p: PathBuf = self.wrkdir.clone(); + match self.perform_shell_cmd_with_path( + p.as_path(), + format!( + "cp -rf \"{}\" \"{}\"; echo $?", + src.get_abs_path().display(), + dst.display() + ) + .as_str(), + ) { + Ok(output) => + // Check if output is 0 + { + match output.as_str().trim() == "0" { + true => Ok(()), // File copied + false => Err(FileTransferError::new_ex( + // Could not copy file + FileTransferErrorType::FileCreateDenied, + format!("\"{}\"", dst.display()), + )), + } + } + Err(err) => Err(FileTransferError::new_ex( + FileTransferErrorType::ProtocolError, + format!("{}", err), + )), + } + } + false => Err(FileTransferError::new( + FileTransferErrorType::UninitializedSession, + )), + } + } + /// ### list_dir /// /// List directory entries diff --git a/src/filetransfer/sftp_transfer.rs b/src/filetransfer/sftp_transfer.rs index 1d199d3..421d149 100644 --- a/src/filetransfer/sftp_transfer.rs +++ b/src/filetransfer/sftp_transfer.rs @@ -348,6 +348,16 @@ impl FileTransfer for SftpFileTransfer { } } + /// ### copy + /// + /// Copy file to destination + fn copy(&mut self, _src: &FsEntry, _dst: &Path) -> Result<(), FileTransferError> { + // SFTP doesn't support file copy + Err(FileTransferError::new( + FileTransferErrorType::UnsupportedFeature, + )) + } + /// ### list_dir /// /// List directory entries @@ -701,6 +711,41 @@ mod tests { assert!(client.disconnect().is_ok()); } + #[test] + fn test_filetransfer_sftp_copy() { + let mut client: SftpFileTransfer = SftpFileTransfer::new(); + assert!(client + .connect( + String::from("test.rebex.net"), + 22, + Some(String::from("demo")), + Some(String::from("password")) + ) + .is_ok()); + // Check session and sftp + assert!(client.session.is_some()); + assert!(client.sftp.is_some()); + assert_eq!(client.wrkdir, PathBuf::from("/")); + // Copy + let file: FsFile = FsFile { + name: String::from("readme.txt"), + abs_path: PathBuf::from("/readme.txt"), + last_change_time: SystemTime::UNIX_EPOCH, + last_access_time: SystemTime::UNIX_EPOCH, + creation_time: SystemTime::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!(client + .copy(&FsEntry::File(file), &Path::new("/tmp/dest.txt")) + .is_err()); + } + #[test] fn test_filetransfer_sftp_cwd_error() { let mut client: SftpFileTransfer = SftpFileTransfer::new(); diff --git a/src/host/mod.rs b/src/host/mod.rs index 2adf0b0..3eab785 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -28,9 +28,9 @@ use std::path::{Path, PathBuf}; use std::time::SystemTime; // Metadata ext #[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))] -use std::os::unix::fs::{MetadataExt, PermissionsExt}; -#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))] use std::fs::set_permissions; +#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))] +use std::os::unix::fs::{MetadataExt, PermissionsExt}; // Locals use crate::fs::{FsDirectory, FsEntry, FsFile}; @@ -187,10 +187,7 @@ impl Localhost { Ok(_) => { // Update dir if dir_name.is_relative() { - self.files = match self.scan_dir(self.wrkdir.as_path()) { - Ok(f) => f, - Err(err) => return Err(err), - }; + self.files = self.scan_dir(self.wrkdir.as_path())?; } Ok(()) } @@ -212,10 +209,7 @@ impl Localhost { match std::fs::remove_dir_all(dir.abs_path.as_path()) { Ok(_) => { // Update dir - self.files = match self.scan_dir(self.wrkdir.as_path()) { - Ok(f) => f, - Err(err) => return Err(err), - }; + self.files = self.scan_dir(self.wrkdir.as_path())?; Ok(()) } Err(err) => Err(HostError::new(HostErrorType::DeleteFailed, Some(err))), @@ -230,10 +224,7 @@ impl Localhost { match std::fs::remove_file(file.abs_path.as_path()) { Ok(_) => { // Update dir - self.files = match self.scan_dir(self.wrkdir.as_path()) { - Ok(f) => f, - Err(err) => return Err(err), - }; + self.files = self.scan_dir(self.wrkdir.as_path())?; Ok(()) } Err(err) => Err(HostError::new(HostErrorType::DeleteFailed, Some(err))), @@ -250,16 +241,85 @@ impl Localhost { match std::fs::rename(abs_path.as_path(), dst_path) { Ok(_) => { // Scan dir - self.files = match self.scan_dir(self.wrkdir.as_path()) { - Ok(f) => f, - Err(err) => return Err(err), - }; + self.files = self.scan_dir(self.wrkdir.as_path())?; Ok(()) } Err(err) => Err(HostError::new(HostErrorType::CouldNotCreateFile, Some(err))), } } + /// ### copy + /// + /// Copy file to destination path + pub fn copy(&mut self, entry: &FsEntry, dst: &Path) -> Result<(), HostError> { + // Get absolute path of dest + let dst: PathBuf = match dst.is_absolute() { + true => PathBuf::from(dst), + false => { + let mut p: PathBuf = self.wrkdir.clone(); + p.push(dst); + p + } + }; + // Match entry + match entry { + FsEntry::File(file) => { + // Copy file + // If destination path is a directory, push file name + let dst: PathBuf = match dst.as_path().is_dir() { + true => { + let mut p: PathBuf = dst.clone(); + p.push(file.name.as_str()); + p + } + false => dst.clone(), + }; + // Copy entry path to dst path + if let Err(err) = std::fs::copy(file.abs_path.as_path(), dst.as_path()) { + return Err(HostError::new(HostErrorType::CouldNotCreateFile, Some(err))); + } + } + FsEntry::Directory(dir) => { + // If destination path doesn't exist, create destination + if !dst.exists() { + self.mkdir(dst.as_path())?; + } + // Scan dir + let dir_files: Vec = self.scan_dir(dir.abs_path.as_path())?; + // Iterate files + for dir_entry in dir_files.iter() { + // Calculate dst + let mut sub_dst: PathBuf = dst.clone(); + sub_dst.push(dir_entry.get_name()); + // Call function recursively + self.copy(dir_entry, sub_dst.as_path())?; + } + } + } + // Reload directory if dst is pwd + match dst.is_dir() { + true => { + if dst == self.pwd().as_path() { + self.files = self.scan_dir(self.wrkdir.as_path())?; + } else if let Some(parent) = dst.parent() { + // If parent is pwd, scan directory + if parent == self.pwd().as_path() { + self.files = self.scan_dir(self.wrkdir.as_path())?; + } + } + } + false => { + if let Some(parent) = dst.parent() { + // If parent is pwd, scan directory + if parent == self.pwd().as_path() { + self.files = self.scan_dir(self.wrkdir.as_path())?; + } + } + } + } + Ok(()) + } + /// ### stat /// /// Stat file and create a FsEntry @@ -768,6 +828,90 @@ mod tests { .is_err()); } + #[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))] + #[test] + fn test_host_copy_file() { + let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); + // Create file in tmpdir + let mut file1_path: PathBuf = PathBuf::from(tmpdir.path()); + file1_path.push("foo.txt"); + // Write file 1 + let mut file1: File = File::create(file1_path.as_path()).ok().unwrap(); + assert!(file1.write_all(b"Hello world!\n").is_ok()); + // Get file 2 path + let mut file2_path: PathBuf = PathBuf::from(tmpdir.path()); + file2_path.push("bar.txt"); + // Create host + let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); + let file1_entry: FsEntry = host.files.get(0).unwrap().clone(); + assert_eq!(file1_entry.get_name(), String::from("foo.txt")); + // Copy + assert!(host.copy(&file1_entry, file2_path.as_path()).is_ok()); + // Verify host has two files + assert_eq!(host.files.len(), 2); + } + + #[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))] + #[test] + fn test_hop_copy_directory_absolute() { + let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); + // Create directory in tmpdir + let mut dir_src: PathBuf = PathBuf::from(tmpdir.path()); + dir_src.push("test_dir/"); + assert!(std::fs::create_dir(dir_src.as_path()).is_ok()); + // Create file in src dir + let mut file1_path: PathBuf = dir_src.clone(); + file1_path.push("foo.txt"); + // Write file 1 + let mut file1: File = File::create(file1_path.as_path()).ok().unwrap(); + assert!(file1.write_all(b"Hello world!\n").is_ok()); + // Copy dir src to dir ddest + let mut dir_dest: PathBuf = PathBuf::from(tmpdir.path()); + dir_dest.push("test_dest_dir/"); + // Create host + let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); + let dir_src_entry: FsEntry = host.files.get(0).unwrap().clone(); + assert_eq!(dir_src_entry.get_name(), String::from("")); + // Copy + assert!(host.copy(&dir_src_entry, dir_dest.as_path()).is_ok()); + // Verify host has two files + assert_eq!(host.files.len(), 2); + // Verify dir_dest contains foo.txt + let mut test_file_path: PathBuf = dir_dest.clone(); + test_file_path.push("foo.txt"); + assert!(host.stat(test_file_path.as_path()).is_ok()); + } + + #[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))] + #[test] + fn test_hop_copy_directory_relative() { + let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); + // Create directory in tmpdir + let mut dir_src: PathBuf = PathBuf::from(tmpdir.path()); + dir_src.push("test_dir/"); + assert!(std::fs::create_dir(dir_src.as_path()).is_ok()); + // Create file in src dir + let mut file1_path: PathBuf = dir_src.clone(); + file1_path.push("foo.txt"); + // Write file 1 + let mut file1: File = File::create(file1_path.as_path()).ok().unwrap(); + assert!(file1.write_all(b"Hello world!\n").is_ok()); + // Copy dir src to dir ddest + let dir_dest: PathBuf = PathBuf::from("test_dest_dir/"); + // Create host + let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); + let dir_src_entry: FsEntry = host.files.get(0).unwrap().clone(); + assert_eq!(dir_src_entry.get_name(), String::from("")); + // Copy + assert!(host.copy(&dir_src_entry, dir_dest.as_path()).is_ok()); + // Verify host has two files + assert_eq!(host.files.len(), 2); + // Verify dir_dest contains foo.txt + let mut test_file_path: PathBuf = dir_dest.clone(); + test_file_path.push("foo.txt"); + assert!(host.stat(test_file_path.as_path()).is_ok()); + } + #[test] fn test_host_fmt_error() { let err: HostError = HostError::new(