diff --git a/CHANGELOG.md b/CHANGELOG.md index edc3d5b..0bc4276 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ Released on ?? - Updated `crossterm` to `0.20` - Updated `open` to `2.0.1` - Added `tui-realm-stdlib 0.6.0` + - Replaced `ftp4` with `suppaftp 4.1.1` - Updated `tui-realm` to `0.6.0` ## 0.6.0 diff --git a/Cargo.lock b/Cargo.lock index 1517e69..ea2f5b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -409,18 +409,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "ftp4" -version = "4.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e03634a7a0e74618f9adf1e088495caa54ea07e72d449813e6439ce8ac9906f" -dependencies = [ - "chrono", - "lazy_static", - "native-tls", - "regex", -] - [[package]] name = "generic-array" version = "0.14.4" @@ -521,9 +509,9 @@ checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" [[package]] name = "js-sys" -version = "0.3.52" +version = "0.3.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce791b7ca6638aae45be056e068fc756d871eb3b3b10b8efa62d1c9cec616752" +checksum = "e4bf49d50e2961077d9c99f4b7997d770a1114f087c3c2e0069b36c13fc2979d" dependencies = [ "wasm-bindgen", ] @@ -647,9 +635,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" [[package]] name = "mio" @@ -800,9 +788,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.35" +version = "0.10.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "549430950c79ae24e6d02e0b7404534ecf311d94cc9f861e9e4020187d13d885" +checksum = "8d9facdb76fec0b73c406f125d44d86fdad818d66fef0531eec9233ca425ff4a" dependencies = [ "bitflags", "cfg-if 1.0.0", @@ -820,9 +808,9 @@ checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" [[package]] name = "openssl-sys" -version = "0.9.65" +version = "0.9.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a7907e3bfa08bb85105209cdfcb6c63d109f8f6c1ed6ca318fff5c1853fbc1d" +checksum = "1996d2d305e561b70d1ee0c53f1542833f4e1ac6ce9a6708b6ff2738ca67dc82" dependencies = [ "autocfg", "cc", @@ -1334,6 +1322,19 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +[[package]] +name = "suppaftp" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d8310cb2dcc312f9e941d35453a1b2cb654186f4ec60b02e2d778a221c3002b" +dependencies = [ + "chrono", + "lazy_static", + "native-tls", + "regex", + "thiserror", +] + [[package]] name = "syn" version = "1.0.74" @@ -1380,7 +1381,6 @@ dependencies = [ "crossterm", "dirs", "edit", - "ftp4", "hostname", "keyring", "lazy_static", @@ -1395,6 +1395,7 @@ dependencies = [ "serde", "simplelog", "ssh2", + "suppaftp", "tempfile", "textwrap", "thiserror", @@ -1638,9 +1639,9 @@ checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" [[package]] name = "wasm-bindgen" -version = "0.2.75" +version = "0.2.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b608ecc8f4198fe8680e2ed18eccab5f0cd4caaf3d83516fa5fb2e927fda2586" +checksum = "8ce9b1b516211d33767048e5d47fa2a381ed8b76fc48d2ce4aa39877f9f183e0" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-macro", @@ -1648,9 +1649,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.75" +version = "0.2.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "580aa3a91a63d23aac5b6b267e2d13cb4f363e31dce6c352fca4752ae12e479f" +checksum = "cfe8dc78e2326ba5f845f4b5bf548401604fa20b1dd1d365fb73b6c1d6364041" dependencies = [ "bumpalo", "lazy_static", @@ -1663,9 +1664,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.75" +version = "0.2.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "171ebf0ed9e1458810dfcb31f2e766ad6b3a89dbda42d8901f2b268277e5f09c" +checksum = "44468aa53335841d9d6b6c023eaab07c0cd4bddbcfdee3e2bb1e8d2cb8069fef" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1673,9 +1674,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.75" +version = "0.2.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c2657dd393f03aa2a659c25c6ae18a13a4048cebd220e147933ea837efc589f" +checksum = "0195807922713af1e67dc66132c7328206ed9766af3858164fb583eedc25fbad" dependencies = [ "proc-macro2", "quote", @@ -1686,15 +1687,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.75" +version = "0.2.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e0c4a743a309662d45f4ede961d7afa4ba4131a59a639f29b0069c3798bbcc2" +checksum = "acdb075a845574a1fa5f09fd77e43f7747599301ea3417a9fbffdeedfc1f4a29" [[package]] name = "web-sys" -version = "0.3.52" +version = "0.3.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c70a82d842c9979078c772d4a1344685045f1a5628f677c2b2eab4dd7d2696" +checksum = "224b2f6b67919060055ef1a67807367c2066ed520c3862cc013d26cf893a783c" dependencies = [ "js-sys", "wasm-bindgen", @@ -1732,9 +1733,9 @@ dependencies = [ [[package]] name = "whoami" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4abacf325c958dfeaf1046931d37f2a901b6dfe0968ee965a29e94c6766b2af6" +checksum = "f7741161a40200a867c96dfa5574544efa4178cf4c8f770b62dd1cc0362d7ae1" dependencies = [ "wasm-bindgen", "web-sys", diff --git a/Cargo.toml b/Cargo.toml index 7216e80..ec78301 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,6 @@ content_inspector = "0.2.4" crossterm = "0.20" dirs = "3.0.1" edit = "0.1.3" -ftp4 = { version = "4.0.2", features = [ "secure" ] } hostname = "0.3.1" keyring = { version = "0.10.1", optional = true } lazy_static = "1.4.0" @@ -48,6 +47,7 @@ rpassword = "5.0.1" serde = { version = "^1.0.0", features = [ "derive" ] } simplelog = "0.10.0" ssh2 = "0.9.0" +suppaftp = { version = "4.1.1", features = [ "secure" ] } tempfile = "3.1.0" textwrap = "0.14.2" thiserror = "^1.0.0" diff --git a/README.md b/README.md index 341ef9b..49853a2 100644 --- a/README.md +++ b/README.md @@ -160,8 +160,8 @@ termscp is powered by these aweseome projects: - [keyring-rs](https://github.com/hwchen/keyring-rs) - [open-rs](https://github.com/Byron/open-rs) - [rpassword](https://github.com/conradkleinespel/rpassword) -- [rust-ftp](https://github.com/mattnenterprise/rust-ftp) - [ssh2-rs](https://github.com/alexcrichton/ssh2-rs) +- [suppaftp](https://github.com/veeso/suppaftp) - [textwrap](https://github.com/mgeisler/textwrap) - [tui-rs](https://github.com/fdehau/tui-rs) - [tui-realm](https://github.com/veeso/tui-realm) diff --git a/src/filetransfer/ftp_transfer.rs b/src/filetransfer/ftp_transfer.rs index 829da89..32d96e3 100644 --- a/src/filetransfer/ftp_transfer.rs +++ b/src/filetransfer/ftp_transfer.rs @@ -27,18 +27,19 @@ */ use super::{FileTransfer, FileTransferError, FileTransferErrorType}; use crate::fs::{FsDirectory, FsEntry, FsFile}; -use crate::utils::fmt::{fmt_time, shadow_password}; -use crate::utils::parser::{parse_datetime, parse_lstime}; +use crate::utils::fmt::shadow_password; // Includes -use ftp4::native_tls::TlsConnector; -use ftp4::{types::FileType, FtpStream}; -use regex::Regex; +use std::convert::TryFrom; +use std::io::{Read, Write}; use std::path::{Path, PathBuf}; -use std::time::SystemTime; -use std::{ - io::{Read, Write}, - ops::Range, +use std::time::UNIX_EPOCH; +use suppaftp::native_tls::TlsConnector; +use suppaftp::{ + list::{File, UnixPexQuery}, + status::FILE_UNAVAILABLE, + types::{FileType, Response}, + FtpError, FtpStream, }; /// ## FtpFileTransfer @@ -71,319 +72,120 @@ impl FtpFileTransfer { p.to_path_buf() } - /// ### parse_list_line + /// ### parse_list_lines /// - /// Parse a line of LIST command output and instantiates an FsEntry from it - fn parse_list_line(&mut self, path: &Path, line: &str) -> Result { - // Try to parse using UNIX syntax - match self.parse_unix_list_line(path, line) { - Ok(entry) => Ok(entry), - Err(_) => match self.parse_dos_list_line(path, line) { - // If UNIX parsing fails, try DOS - Ok(entry) => Ok(entry), - Err(_) => Err(()), - }, - } + /// 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() } - /// ### parse_unix_list_line + /// ### get_symlink_entry /// - /// Try to parse a "LIST" output command line in UNIX format. - /// Returns error if syntax is not UNIX compliant. - /// UNIX syntax has the following syntax: - /// {FILE_TYPE}{UNIX_PEX} {HARD_LINKS} {USER} {GROUP} {SIZE} {DATE} {FILENAME} - /// -rw-r--r-- 1 cvisintin staff 4968 27 Dic 10:46 CHANGELOG.md - fn parse_unix_list_line(&mut self, path: &Path, line: &str) -> Result { - // Prepare list regex - // NOTE: about this damn regex - lazy_static! { - static ref LS_RE: Regex = Regex::new(r#"^([\-ld])([\-rwxs]{9})\s+(\d+)\s+(\w+)\s+(\w+)\s+(\d+)\s+(\w{3}\s+\d{1,2}\s+(?:\d{1,2}:\d{1,2}|\d{4}))\s+(.+)$"#).unwrap(); - } - debug!("Parsing LIST (UNIX) line: '{}'", line); - // Apply regex to result - match LS_RE.captures(line) { - // String matches regex - Some(metadata) => { - // NOTE: metadata fmt: (regex, file_type, permissions, link_count, uid, gid, filesize, mtime, filename) - // Expected 7 + 1 (8) values: + 1 cause regex is repeated at 0 - if metadata.len() < 8 { - return Err(()); - } - // Collect metadata - // Get if is directory and if is symlink - let (mut is_dir, is_symlink): (bool, bool) = match metadata.get(1).unwrap().as_str() - { - "-" => (false, false), - "l" => (false, true), - "d" => (true, false), - _ => return Err(()), // Ignore special files - }; - // Check string length (unix pex) - if metadata.get(2).unwrap().as_str().len() < 9 { - return Err(()); - } - - let pex = |range: Range| { - let mut count: u8 = 0; - for (i, c) in metadata.get(2).unwrap().as_str()[range].chars().enumerate() { - match c { - '-' => {} - _ => { - count += match i { - 0 => 4, - 1 => 2, - 2 => 1, - _ => 0, - } - } - } + /// 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 } - count }; - - // Get unix pex - let unix_pex = (pex(0..3), pex(3..6), pex(6..9)); - - // Parse mtime and convert to SystemTime - let mtime: SystemTime = match parse_lstime( - metadata.get(7).unwrap().as_str(), - "%b %d %Y", - "%b %d %H:%M", - ) { - Ok(t) => t, - Err(_) => SystemTime::UNIX_EPOCH, - }; - // Get uid - let uid: Option = match metadata.get(4).unwrap().as_str().parse::() { - Ok(uid) => Some(uid), - Err(_) => None, - }; - // Get gid - let gid: Option = match metadata.get(5).unwrap().as_str().parse::() { - Ok(gid) => Some(gid), - Err(_) => None, - }; - // Get filesize - let filesize: usize = metadata - .get(6) - .unwrap() - .as_str() - .parse::() - .unwrap_or(0); - // Split filename if required - let (file_name, symlink_path): (String, Option) = match is_symlink { - true => self.get_name_and_link(metadata.get(8).unwrap().as_str()), - false => (String::from(metadata.get(8).unwrap().as_str()), None), - }; - // Check if file_name is '.' or '..' - if file_name.as_str() == "." || file_name.as_str() == ".." { - debug!("File name is {}; ignoring entry", file_name); - return Err(()); - } - // Get symlink - let symlink: Option> = symlink_path.map(|p| { - Box::new(match p.to_string_lossy().ends_with('/') { - true => { - // NOTE: is_dir becomes true - is_dir = true; - FsEntry::Directory(FsDirectory { - name: p - .file_name() - .unwrap_or_else(|| std::ffi::OsStr::new("")) - .to_string_lossy() - .to_string(), - abs_path: p.clone(), - last_change_time: mtime, - last_access_time: mtime, - creation_time: mtime, - readonly: false, - symlink: None, - user: uid, - group: gid, - unix_pex: Some(unix_pex), - }) - } - false => FsEntry::File(FsFile { - name: p - .file_name() - .unwrap_or_else(|| std::ffi::OsStr::new("")) - .to_string_lossy() - .to_string(), - abs_path: p.clone(), - last_change_time: mtime, - last_access_time: mtime, - creation_time: mtime, - readonly: false, - symlink: None, - size: filesize, - ftype: p.extension().map(|s| String::from(s.to_string_lossy())), - user: uid, - group: gid, - unix_pex: Some(unix_pex), - }), - }) - }); - let mut abs_path: PathBuf = PathBuf::from(path); - abs_path.push(file_name.as_str()); - let abs_path: PathBuf = Self::resolve(abs_path.as_path()); - // get extension - let extension: Option = abs_path - .as_path() - .extension() - .map(|s| String::from(s.to_string_lossy())); - // Return - debug!("Follows LIST line '{}' attributes", line); - debug!("Is directory? {}", is_dir); - debug!("Is symlink? {}", is_symlink); - debug!("name: {}", file_name); - debug!("abs_path: {}", abs_path.display()); - debug!("last_change_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S")); - debug!("last_access_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S")); - debug!("creation_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S")); - debug!("symlink: {:?}", symlink); - debug!("user: {:?}", uid); - debug!("group: {:?}", gid); - debug!("unix_pex: {:?}", unix_pex); - debug!("---------------------------------------"); - // Push to entries - Ok(match is_dir { - true => FsEntry::Directory(FsDirectory { - name: file_name, - abs_path, - last_change_time: mtime, - last_access_time: mtime, - creation_time: mtime, - readonly: false, - symlink, - user: uid, - group: gid, - unix_pex: Some(unix_pex), - }), - false => FsEntry::File(FsFile { - name: file_name, - abs_path, - last_change_time: mtime, - last_access_time: mtime, - creation_time: mtime, - size: filesize, - ftype: extension, - readonly: false, - symlink, - user: uid, - group: gid, - unix_pex: Some(unix_pex), - }), - }) + 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, + }))) } - None => Err(()), } } - /// ### parse_dos_list_line + /// ### query_unix_pex /// - /// Try to parse a "LIST" output command line in DOS format. - /// Returns error if syntax is not DOS compliant. - /// DOS syntax has the following syntax: - /// {DATE} {TIME} { | SIZE} {FILENAME} - /// 10-19-20 03:19PM pub - /// 04-08-14 03:09PM 403 readme.txt - fn parse_dos_list_line(&self, path: &Path, line: &str) -> Result { - // Prepare list regex - // NOTE: you won't find this regex on the internet. It seems I'm the only person in the world who needs this - lazy_static! { - static ref DOS_RE: Regex = Regex::new( - r#"^(\d{2}\-\d{2}\-\d{2}\s+\d{2}:\d{2}\s*[AP]M)\s+()?([\d,]*)\s+(.+)$"# - ) - .unwrap(); - } - debug!("Parsing LIST (DOS) line: '{}'", line); - // Apply regex to result - match DOS_RE.captures(line) { - // String matches regex - Some(metadata) => { - // NOTE: metadata fmt: (regex, date_time, is_dir?, file_size?, file_name) - // Expected 4 + 1 (5) values: + 1 cause regex is repeated at 0 - if metadata.len() < 5 { - return Err(()); - } - // Parse date time - let time: SystemTime = - match parse_datetime(metadata.get(1).unwrap().as_str(), "%d-%m-%y %I:%M%p") { - Ok(t) => t, - Err(_) => SystemTime::UNIX_EPOCH, // Don't return error - }; - // Get if is a directory - let is_dir: bool = metadata.get(2).is_some(); - // Get file size - let file_size: usize = match is_dir { - true => 0, // If is directory, filesize is 0 - false => match metadata.get(3) { - // If is file, parse arg 3 - Some(val) => val.as_str().parse::().unwrap_or(0), - None => 0, // Should not happen - }, - }; - // Get file name - let file_name: String = String::from(metadata.get(4).unwrap().as_str()); - // Get absolute path - let mut abs_path: PathBuf = PathBuf::from(path); - abs_path.push(file_name.as_str()); - let abs_path: PathBuf = Self::resolve(abs_path.as_path()); - // Get extension - let extension: Option = abs_path - .as_path() - .extension() - .map(|s| String::from(s.to_string_lossy())); - debug!("Follows LIST line '{}' attributes", line); - debug!("Is directory? {}", is_dir); - debug!("name: {}", file_name); - debug!("abs_path: {}", abs_path.display()); - debug!("last_change_time: {}", fmt_time(time, "%Y-%m-%dT%H:%M:%S")); - debug!("last_access_time: {}", fmt_time(time, "%Y-%m-%dT%H:%M:%S")); - debug!("creation_time: {}", fmt_time(time, "%Y-%m-%dT%H:%M:%S")); - debug!("---------------------------------------"); - // Return entry - Ok(match is_dir { - true => FsEntry::Directory(FsDirectory { - name: file_name, - abs_path, - last_change_time: time, - last_access_time: time, - creation_time: time, - readonly: false, - symlink: None, - user: None, - group: None, - unix_pex: None, - }), - false => FsEntry::File(FsFile { - name: file_name, - abs_path, - last_change_time: time, - last_access_time: time, - creation_time: time, - size: file_size, - ftype: extension, - readonly: false, - symlink: None, - user: None, - group: None, - unix_pex: None, - }), - }) - } - None => Err(()), // Invalid syntax - } + /// Returns unix pex in tuple of values + fn query_unix_pex(f: &File) -> (u8, u8, u8) { + ( + Self::pex_to_byte( + f.can_read(UnixPexQuery::Owner), + f.can_write(UnixPexQuery::Owner), + f.can_execute(UnixPexQuery::Owner), + ), + Self::pex_to_byte( + f.can_read(UnixPexQuery::Group), + f.can_write(UnixPexQuery::Group), + f.can_execute(UnixPexQuery::Group), + ), + Self::pex_to_byte( + f.can_read(UnixPexQuery::Others), + f.can_write(UnixPexQuery::Others), + f.can_execute(UnixPexQuery::Others), + ), + ) } - /// ### get_name_and_link + /// ### pex_to_byte /// - /// Returns from a `ls -l` command output file name token, the name of the file and the symbolic link (if there is any) - fn get_name_and_link(&self, token: &str) -> (String, Option) { - let tokens: Vec<&str> = token.split(" -> ").collect(); - let filename: String = String::from(*tokens.get(0).unwrap()); - let symlink: Option = tokens.get(1).map(PathBuf::from); - (filename, symlink) + /// 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) } } @@ -473,7 +275,12 @@ impl FileTransfer for FtpFileTransfer { self.stream = Some(stream); info!("Connection successfully established"); // Return OK - Ok(self.stream.as_ref().unwrap().get_welcome_msg()) + Ok(self + .stream + .as_ref() + .unwrap() + .get_welcome_msg() + .map(|x| x.to_string())) } /// ### disconnect @@ -567,22 +374,10 @@ impl FileTransfer for FtpFileTransfer { info!("LIST dir {}", dir.display()); match &mut self.stream { Some(stream) => match stream.list(Some(&dir.as_path().to_string_lossy())) { - Ok(entries) => { - debug!("Got {} lines in LIST result", entries.len()); - // Prepare result - let mut result: Vec = Vec::with_capacity(entries.len()); + Ok(lines) => { + debug!("Got {} lines in LIST result", lines.len()); // Iterate over entries - for entry in entries.iter() { - if let Ok(file) = self.parse_list_line(dir.as_path(), entry) { - result.push(file); - } - } - debug!( - "{} out of {} were valid entries", - result.len(), - entries.len() - ); - Ok(result) + Ok(self.parse_list_lines(path, lines)) } Err(err) => Err(FileTransferError::new_ex( FileTransferErrorType::DirStatFailed, @@ -597,13 +392,23 @@ impl FileTransfer for FtpFileTransfer { /// ### mkdir /// - /// Make directory + /// 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::InvalidResponse(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(), @@ -791,7 +596,8 @@ impl FileTransfer for FtpFileTransfer { fn recv_file(&mut self, file: &FsFile) -> Result, FileTransferError> { info!("Receiving file {}", file.abs_path.display()); match &mut self.stream { - Some(stream) => match stream.get(&file.abs_path.as_path().to_string_lossy()) { + 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, @@ -837,7 +643,7 @@ impl FileTransfer for FtpFileTransfer { fn on_recv(&mut self, readable: Box) -> Result<(), FileTransferError> { info!("Finalizing get"); match &mut self.stream { - Some(stream) => match stream.finalize_get(readable) { + Some(stream) => match stream.finalize_retr_stream(readable) { Ok(_) => Ok(()), Err(err) => Err(FileTransferError::new_ex( FileTransferErrorType::ProtocolError, @@ -856,7 +662,6 @@ mod tests { use super::*; use crate::utils::file::open_file; - use crate::utils::fmt::fmt_time; #[cfg(feature = "with-containers")] use crate::utils::test_helpers::write_file; use crate::utils::test_helpers::{create_sample_file_entry, make_fsentry}; @@ -902,6 +707,14 @@ mod tests { 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 @@ -957,9 +770,9 @@ mod tests { let dummy: FsEntry = FsEntry::File(FsFile { name: String::from("cucumber.txt"), abs_path: PathBuf::from("/cucumber.txt"), - last_change_time: SystemTime::UNIX_EPOCH, - last_access_time: SystemTime::UNIX_EPOCH, - creation_time: SystemTime::UNIX_EPOCH, + 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, @@ -1051,12 +864,13 @@ mod tests { let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false); // Simple file let file: FsFile = ftp - .parse_list_line( + .parse_list_lines( PathBuf::from("/tmp").as_path(), - "-rw-rw-r-- 1 root dialout 8192 Nov 5 2018 omar.txt", + vec!["-rw-rw-r-- 1 root dialout 8192 Nov 5 2018 omar.txt".to_string()], ) - .ok() + .get(0) .unwrap() + .clone() .unwrap_file(); assert_eq!(file.abs_path, PathBuf::from("/tmp/omar.txt")); assert_eq!(file.name, String::from("omar.txt")); @@ -1067,180 +881,22 @@ mod tests { assert_eq!(file.unix_pex.unwrap(), (6, 6, 4)); assert_eq!( file.last_access_time - .duration_since(SystemTime::UNIX_EPOCH) + .duration_since(UNIX_EPOCH) .ok() .unwrap(), Duration::from_secs(1541376000) ); assert_eq!( file.last_change_time - .duration_since(SystemTime::UNIX_EPOCH) + .duration_since(UNIX_EPOCH) .ok() .unwrap(), Duration::from_secs(1541376000) ); assert_eq!( - file.creation_time - .duration_since(SystemTime::UNIX_EPOCH) - .ok() - .unwrap(), + file.creation_time.duration_since(UNIX_EPOCH).ok().unwrap(), Duration::from_secs(1541376000) ); - // Simple file with number as gid, uid - let file: FsFile = ftp - .parse_list_line( - PathBuf::from("/tmp").as_path(), - "-rwxr-xr-x 1 0 9 4096 Nov 5 16:32 omar.txt", - ) - .ok() - .unwrap() - .unwrap_file(); - assert_eq!(file.abs_path, PathBuf::from("/tmp/omar.txt")); - assert_eq!(file.name, String::from("omar.txt")); - assert_eq!(file.size, 4096); - assert!(file.symlink.is_none()); - assert_eq!(file.user, Some(0)); - assert_eq!(file.group, Some(9)); - assert_eq!(file.unix_pex.unwrap(), (7, 5, 5)); - assert_eq!( - fmt_time(file.last_access_time, "%m %d %M").as_str(), - "11 05 32" - ); - assert_eq!( - fmt_time(file.last_change_time, "%m %d %M").as_str(), - "11 05 32" - ); - assert_eq!( - fmt_time(file.creation_time, "%m %d %M").as_str(), - "11 05 32" - ); - // Directory - let dir: FsDirectory = ftp - .parse_list_line( - PathBuf::from("/tmp").as_path(), - "drwxrwxr-x 1 0 9 4096 Nov 5 2018 docs", - ) - .ok() - .unwrap() - .unwrap_dir(); - assert_eq!(dir.abs_path, PathBuf::from("/tmp/docs")); - assert_eq!(dir.name, String::from("docs")); - assert!(dir.symlink.is_none()); - assert_eq!(dir.user, Some(0)); - assert_eq!(dir.group, Some(9)); - assert_eq!(dir.unix_pex.unwrap(), (7, 7, 5)); - assert_eq!( - dir.last_access_time - .duration_since(SystemTime::UNIX_EPOCH) - .ok() - .unwrap(), - Duration::from_secs(1541376000) - ); - assert_eq!( - dir.last_change_time - .duration_since(SystemTime::UNIX_EPOCH) - .ok() - .unwrap(), - Duration::from_secs(1541376000) - ); - assert_eq!( - dir.creation_time - .duration_since(SystemTime::UNIX_EPOCH) - .ok() - .unwrap(), - Duration::from_secs(1541376000) - ); - assert_eq!(dir.readonly, false); - // Error - assert!(ftp - .parse_list_line( - PathBuf::from("/").as_path(), - "drwxrwxr-x 1 0 9 Nov 5 2018 docs" - ) - .is_err()); - } - - #[test] - fn test_filetransfer_ftp_parse_list_line_dos() { - let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false); - // Simple file - let file: FsFile = ftp - .parse_list_line( - PathBuf::from("/tmp").as_path(), - "04-08-14 03:09PM 8192 omar.txt", - ) - .ok() - .unwrap() - .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, None); - assert_eq!( - file.last_access_time - .duration_since(SystemTime::UNIX_EPOCH) - .ok() - .unwrap(), - Duration::from_secs(1407164940) - ); - assert_eq!( - file.last_change_time - .duration_since(SystemTime::UNIX_EPOCH) - .ok() - .unwrap(), - Duration::from_secs(1407164940) - ); - assert_eq!( - file.creation_time - .duration_since(SystemTime::UNIX_EPOCH) - .ok() - .unwrap(), - Duration::from_secs(1407164940) - ); - // Directory - let dir: FsDirectory = ftp - .parse_list_line( - PathBuf::from("/tmp").as_path(), - "04-08-14 03:09PM docs", - ) - .ok() - .unwrap() - .unwrap_dir(); - assert_eq!(dir.abs_path, PathBuf::from("/tmp/docs")); - assert_eq!(dir.name, String::from("docs")); - assert!(dir.symlink.is_none()); - assert_eq!(dir.user, None); - assert_eq!(dir.group, None); - assert_eq!(dir.unix_pex, None); - assert_eq!( - dir.last_access_time - .duration_since(SystemTime::UNIX_EPOCH) - .ok() - .unwrap(), - Duration::from_secs(1407164940) - ); - assert_eq!( - dir.last_change_time - .duration_since(SystemTime::UNIX_EPOCH) - .ok() - .unwrap(), - Duration::from_secs(1407164940) - ); - assert_eq!( - dir.creation_time - .duration_since(SystemTime::UNIX_EPOCH) - .ok() - .unwrap(), - Duration::from_secs(1407164940) - ); - assert_eq!(dir.readonly, false); - // Error - assert!(ftp - .parse_list_line(PathBuf::from("/").as_path(), "04-08-14 omar.txt") - .is_err()); } #[test] @@ -1265,27 +921,14 @@ mod tests { assert!(ftp.disconnect().is_ok()); } - #[test] - fn test_filetransfer_ftp_get_name_and_link() { - let client: FtpFileTransfer = FtpFileTransfer::new(false); - assert_eq!( - client.get_name_and_link("Cargo.toml"), - (String::from("Cargo.toml"), None) - ); - assert_eq!( - client.get_name_and_link("Cargo -> Cargo.toml"), - (String::from("Cargo"), Some(PathBuf::from("Cargo.toml"))) - ); - } - #[test] fn test_filetransfer_ftp_uninitialized() { let file: FsFile = FsFile { name: String::from("omar.txt"), abs_path: PathBuf::from("/omar.txt"), - last_change_time: SystemTime::UNIX_EPOCH, - last_access_time: SystemTime::UNIX_EPOCH, - creation_time: SystemTime::UNIX_EPOCH, + 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, diff --git a/src/filetransfer/mod.rs b/src/filetransfer/mod.rs index 79d4a7c..39d10c0 100644 --- a/src/filetransfer/mod.rs +++ b/src/filetransfer/mod.rs @@ -182,7 +182,7 @@ pub trait FileTransfer { /// ### mkdir /// /// Make directory - /// It MUSTN'T return error in case the directory already exists + /// In case the directory already exists, it must return an Error of kind `FileTransferErrorType::DirectoryAlreadyExists` fn mkdir(&mut self, dir: &Path) -> Result<(), FileTransferError>; /// ### remove diff --git a/src/filetransfer/scp_transfer.rs b/src/filetransfer/scp_transfer.rs index e08c0d0..8ff89a7 100644 --- a/src/filetransfer/scp_transfer.rs +++ b/src/filetransfer/scp_transfer.rs @@ -643,7 +643,7 @@ impl FileTransfer for ScpFileTransfer { /// ### mkdir /// /// Make directory - /// It MUSTN'T return error in case the directory already exists + /// In case the directory already exists, it must return an Error of kind `FileTransferErrorType::DirectoryAlreadyExists` fn mkdir(&mut self, dir: &Path) -> Result<(), FileTransferError> { match self.is_connected() { true => { @@ -651,7 +651,7 @@ impl FileTransfer for ScpFileTransfer { info!("Making directory {}", dir.display()); let p: PathBuf = self.wrkdir.clone(); // If directory already exists, return Err - if let Ok(_) = self.stat(dir.as_path()) { + if self.stat(dir.as_path()).is_ok() { error!("Directory {} already exists", dir.display()); return Err(FileTransferError::new( FileTransferErrorType::DirectoryAlreadyExists, @@ -1026,6 +1026,15 @@ mod tests { assert!(client.list_dir(&Path::new("/config")).unwrap().len() >= 4); // Make directory assert!(client.mkdir(PathBuf::from("/tmp/omar").as_path()).is_ok()); + // Remake directory (should report already exists) + assert_eq!( + client + .mkdir(PathBuf::from("/tmp/omar").as_path()) + .err() + .unwrap() + .kind(), + FileTransferErrorType::DirectoryAlreadyExists + ); // Make directory (err) assert!(client .mkdir(PathBuf::from("/root/aaaaa/pommlar").as_path()) diff --git a/src/filetransfer/sftp_transfer.rs b/src/filetransfer/sftp_transfer.rs index b067ee3..64050b3 100644 --- a/src/filetransfer/sftp_transfer.rs +++ b/src/filetransfer/sftp_transfer.rs @@ -554,13 +554,14 @@ impl FileTransfer for SftpFileTransfer { /// ### mkdir /// /// Make directory + /// In case the directory already exists, it must return an Error of kind `FileTransferErrorType::DirectoryAlreadyExists` fn mkdir(&mut self, dir: &Path) -> Result<(), FileTransferError> { match self.sftp.as_ref() { Some(sftp) => { // Make directory let path: PathBuf = self.get_abs_path(PathBuf::from(dir).as_path()); // If directory already exists, return Err - if let Ok(_) = sftp.stat(path.as_path()) { + if sftp.stat(path.as_path()).is_ok() { error!("Directory {} already exists", path.display()); return Err(FileTransferError::new( FileTransferErrorType::DirectoryAlreadyExists, @@ -846,6 +847,15 @@ mod tests { assert!(client.list_dir(&Path::new("/config")).unwrap().len() >= 4); // Make directory assert!(client.mkdir(PathBuf::from("/tmp/omar").as_path()).is_ok()); + // Remake directory (should report already exists) + assert_eq!( + client + .mkdir(PathBuf::from("/tmp/omar").as_path()) + .err() + .unwrap() + .kind(), + FileTransferErrorType::DirectoryAlreadyExists + ); // Make directory (err) assert!(client .mkdir(PathBuf::from("/root/aaaaa/pommlar").as_path()) diff --git a/src/lib.rs b/src/lib.rs index 48bed11..b1f2840 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,7 +38,6 @@ extern crate content_inspector; extern crate crossterm; extern crate dirs; extern crate edit; -extern crate ftp4; extern crate hostname; #[cfg(feature = "with-keyring")] extern crate keyring; @@ -54,6 +53,7 @@ extern crate path_slash; extern crate rand; extern crate regex; extern crate ssh2; +extern crate suppaftp; extern crate tempfile; extern crate textwrap; extern crate tui_realm_stdlib; diff --git a/src/utils/parser.rs b/src/utils/parser.rs index 1cb15c5..7359b44 100644 --- a/src/utils/parser.rs +++ b/src/utils/parser.rs @@ -202,6 +202,7 @@ pub fn parse_lstime(tm: &str, fmt_year: &str, fmt_hours: &str) -> Result Result { match NaiveDateTime::parse_from_str(tm, fmt) { Ok(dt) => {