diff --git a/CHANGELOG.md b/CHANGELOG.md index dcadfb8..4a62a19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Released on FIXME: - [Issue 9](https://github.com/veeso/termscp/issues/9): Fixed issues related to paths on remote when using Windows - Dependencies: - Added `path-slash 0.1.4` (Windows only) + - Added `thiserror 1.0.24` ## 0.4.0 diff --git a/Cargo.lock b/Cargo.lock index 93090d0..f10cc4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1411,6 +1411,7 @@ dependencies = [ "ssh2", "tempfile", "textwrap", + "thiserror", "toml", "tui", "ureq", @@ -1429,6 +1430,26 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "thiserror" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.1.1" diff --git a/Cargo.toml b/Cargo.toml index bda1615..7c73626 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,19 +1,31 @@ [package] -name = "termscp" -version = "0.4.1" authors = ["Christian Visintin"] -edition = "2018" -license = "MIT" -keywords = ["scp-client", "sftp-client", "ftp-client", "winscp", "command-line-utility"] categories = ["command-line-utilities"] description = "TermSCP is a SCP/SFTP/FTPS client for command line with an integrated UI to explore the remote file system. Basically WinSCP on a terminal." -homepage = "https://github.com/veeso/termscp" -repository = "https://github.com/veeso/termscp" documentation = "https://docs.rs/termscp" -readme = "README.md" +edition = "2018" +homepage = "https://github.com/veeso/termscp" include = ["src/**/*", "LICENSE", "README.md", "CHANGELOG.md"] +keywords = ["scp-client", "sftp-client", "ftp-client", "winscp", "command-line-utility"] +license = "MIT" +name = "termscp" +readme = "README.md" +repository = "https://github.com/veeso/termscp" +version = "0.4.1" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[metadata] +[metadata.rpm] +package = "termscp" + +[metadata.rpm.cargo] +buildflags = ["--release"] + +[metadata.rpm.targets] +[metadata.rpm.targets.termscp] +path = "/usr/bin/termscp" +[[bin]] +name = "termscp" +path = "src/main.rs" [dependencies] bitflags = "1.2.1" @@ -23,7 +35,6 @@ content_inspector = "0.2.4" crossterm = "0.19.0" dirs = "3.0.1" edit = "0.1.2" -ftp4 = { version = "^4.0.2", features = ["secure"] } getopts = "0.2.21" hostname = "0.3.1" lazy_static = "1.4.0" @@ -31,41 +42,43 @@ magic-crypt = "3.1.6" rand = "0.8.2" regex = "1.4.2" rpassword = "5.0.1" -serde = { version = "1.0.121", features = ["derive"] } ssh2 = "0.9.0" tempfile = "3.1.0" textwrap = "0.13.1" +thiserror = "^1.0.0" toml = "0.5.8" -tui = { version = "0.14.0", features = ["crossterm"], default-features = false } -ureq = { version = "2.0.2", features = ["json"] } whoami = "1.1.0" wildmatch = "1.0.13" -# POSIX dependencies -[target.'cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))'.dependencies] +[dependencies.ftp4] +features = ["secure"] +version = "^4.0.2" + +[dependencies.serde] +features = ["derive"] +version = "1.0.121" + +[dependencies.tui] +default-features = false +features = ["crossterm"] +version = "0.14.0" + +[dependencies.ureq] +features = ["json"] +version = "2.0.2" + +[features] +githubActions = [] + +[target] +[target."cfg(any(target_os = \"unix\", target_os = \"macos\", target_os = \"linux\"))"] +[target."cfg(any(target_os = \"unix\", target_os = \"macos\", target_os = \"linux\"))".dependencies] users = "0.11.0" -# Windows + MacOS -[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies] +[target."cfg(any(target_os = \"windows\", target_os = \"macos\"))"] +[target."cfg(any(target_os = \"windows\", target_os = \"macos\"))".dependencies] keyring = "0.10.1" -# Windows dependencies -[target.'cfg(target_os = "windows")'.dependencies] +[target."cfg(target_os = \"windows\")"] +[target."cfg(target_os = \"windows\")".dependencies] path-slash = "0.1.4" - -# Features -[features] -githubActions = [] # used to run particular on github actions - -[[bin]] -name = "termscp" -path = "src/main.rs" - -[package.metadata.rpm] -package = "termscp" - -[package.metadata.rpm.cargo] -buildflags = ["--release"] - -[package.metadata.rpm.targets] -termscp = { path = "/usr/bin/termscp" } diff --git a/src/host/mod.rs b/src/host/mod.rs index d5ad17e..68abbbc 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -31,6 +31,7 @@ extern crate wildmatch; use std::fs::{self, File, Metadata, OpenOptions}; use std::path::{Path, PathBuf}; use std::time::SystemTime; +use thiserror::Error; use wildmatch::WildMatch; // Metadata ext #[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))] @@ -44,15 +45,23 @@ use crate::fs::{FsDirectory, FsEntry, FsFile}; /// ## HostErrorType /// /// HostErrorType provides an overview of the specific host error -#[derive(PartialEq, std::fmt::Debug)] +#[derive(Error, Debug)] pub enum HostErrorType { + #[error("No such file or directory")] NoSuchFileOrDirectory, + #[error("File is readonly")] ReadonlyFile, + #[error("Could not access directory")] DirNotAccessible, + #[error("Could not access file")] FileNotAccessible, + #[error("File already exists")] FileAlreadyExists, + #[error("Could not create file")] CouldNotCreateFile, + #[error("Command execution failed")] ExecutionFailed, + #[error("Could not delete file")] DeleteFailed, } @@ -62,36 +71,42 @@ pub enum HostErrorType { pub struct HostError { pub error: HostErrorType, - pub ioerr: Option, + ioerr: Option, + path: Option, } impl HostError { /// ### new /// /// Instantiates a new HostError - pub(crate) fn new(error: HostErrorType, errno: Option) -> HostError { + pub(crate) fn new(error: HostErrorType, errno: Option, p: &Path) -> Self { HostError { error, ioerr: errno, + path: Some(p.to_path_buf()) + } + } +} + +impl From for HostError { + fn from(error: HostErrorType) -> Self { + HostError { + error, + ioerr: None, + path: None, } } } impl std::fmt::Display for HostError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - let code_str: &str = match self.error { - HostErrorType::NoSuchFileOrDirectory => "No such file or directory", - HostErrorType::ReadonlyFile => "File is readonly", - HostErrorType::DirNotAccessible => "Could not access directory", - HostErrorType::FileNotAccessible => "Could not access file", - HostErrorType::FileAlreadyExists => "File already exists", - HostErrorType::CouldNotCreateFile => "Could not create file", - HostErrorType::ExecutionFailed => "Could not run command", - HostErrorType::DeleteFailed => "Could not delete file", + let p_str: String = match self.path.as_ref() { + None => String::new(), + Some(p) => format!(" ({})", p.display().to_string()), }; match &self.ioerr { - Some(err) => write!(f, "{}: {}", code_str, err), - None => write!(f, "{}", code_str), + Some(err) => write!(f, "{}: {}{}", self.error, err, p_str), + None => write!(f, "{}{}", self.error, p_str), } } } @@ -116,7 +131,7 @@ impl Localhost { }; // Check if dir exists if !host.file_exists(host.wrkdir.as_path()) { - return Err(HostError::new(HostErrorType::NoSuchFileOrDirectory, None)); + return Err(HostError::new(HostErrorType::NoSuchFileOrDirectory, None, host.wrkdir.as_path())); } // Retrieve files for provided path host.files = match host.scan_dir(host.wrkdir.as_path()) { @@ -148,11 +163,11 @@ impl Localhost { let new_dir: PathBuf = self.to_abs_path(new_dir); // Check whether directory exists if !self.file_exists(new_dir.as_path()) { - return Err(HostError::new(HostErrorType::NoSuchFileOrDirectory, None)); + return Err(HostError::new(HostErrorType::NoSuchFileOrDirectory, None, new_dir.as_path())); } // Change directory if std::env::set_current_dir(new_dir.as_path()).is_err() { - return Err(HostError::new(HostErrorType::NoSuchFileOrDirectory, None)); + return Err(HostError::new(HostErrorType::NoSuchFileOrDirectory, None, new_dir.as_path())); } let prev_dir: PathBuf = self.wrkdir.clone(); // Backup location // Update working directory @@ -187,10 +202,10 @@ impl Localhost { if dir_path.exists() { match ignex { true => return Ok(()), - false => return Err(HostError::new(HostErrorType::FileAlreadyExists, None)), + false => return Err(HostError::new(HostErrorType::FileAlreadyExists, None, dir_path.as_path())), } } - match std::fs::create_dir(dir_path) { + match std::fs::create_dir(dir_path.as_path()) { Ok(_) => { // Update dir if dir_name.is_relative() { @@ -198,7 +213,7 @@ impl Localhost { } Ok(()) } - Err(err) => Err(HostError::new(HostErrorType::CouldNotCreateFile, Some(err))), + Err(err) => Err(HostError::new(HostErrorType::CouldNotCreateFile, Some(err), dir_path.as_path())), } } @@ -210,7 +225,7 @@ impl Localhost { FsEntry::Directory(dir) => { // If file doesn't exist; return error if !dir.abs_path.as_path().exists() { - return Err(HostError::new(HostErrorType::NoSuchFileOrDirectory, None)); + return Err(HostError::new(HostErrorType::NoSuchFileOrDirectory, None, dir.abs_path.as_path())); } // Remove match std::fs::remove_dir_all(dir.abs_path.as_path()) { @@ -219,13 +234,13 @@ impl Localhost { self.files = self.scan_dir(self.wrkdir.as_path())?; Ok(()) } - Err(err) => Err(HostError::new(HostErrorType::DeleteFailed, Some(err))), + Err(err) => Err(HostError::new(HostErrorType::DeleteFailed, Some(err), dir.abs_path.as_path())), } } FsEntry::File(file) => { // If file doesn't exist; return error if !file.abs_path.as_path().exists() { - return Err(HostError::new(HostErrorType::NoSuchFileOrDirectory, None)); + return Err(HostError::new(HostErrorType::NoSuchFileOrDirectory, None, file.abs_path.as_path())); } // Remove match std::fs::remove_file(file.abs_path.as_path()) { @@ -234,7 +249,7 @@ impl Localhost { self.files = self.scan_dir(self.wrkdir.as_path())?; Ok(()) } - Err(err) => Err(HostError::new(HostErrorType::DeleteFailed, Some(err))), + Err(err) => Err(HostError::new(HostErrorType::DeleteFailed, Some(err), file.abs_path.as_path())), } } } @@ -251,7 +266,7 @@ impl Localhost { self.files = self.scan_dir(self.wrkdir.as_path())?; Ok(()) } - Err(err) => Err(HostError::new(HostErrorType::CouldNotCreateFile, Some(err))), + Err(err) => Err(HostError::new(HostErrorType::CouldNotCreateFile, Some(err), abs_path.as_path())), } } @@ -276,7 +291,7 @@ impl Localhost { }; // 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))); + return Err(HostError::new(HostErrorType::CouldNotCreateFile, Some(err), file.abs_path.as_path())); } } FsEntry::Directory(dir) => { @@ -328,7 +343,7 @@ impl Localhost { let path: PathBuf = self.to_abs_path(path); let attr: Metadata = match fs::metadata(path.as_path()) { Ok(metadata) => metadata, - Err(err) => return Err(HostError::new(HostErrorType::FileNotAccessible, Some(err))), + Err(err) => return Err(HostError::new(HostErrorType::FileNotAccessible, Some(err), path.as_path())), }; let file_name: String = String::from(path.file_name().unwrap().to_str().unwrap_or("")); // Match dir / file @@ -389,7 +404,7 @@ impl Localhost { let path: PathBuf = self.to_abs_path(path); let attr: Metadata = match fs::metadata(path.as_path()) { Ok(metadata) => metadata, - Err(err) => return Err(HostError::new(HostErrorType::FileNotAccessible, Some(err))), + Err(err) => return Err(HostError::new(HostErrorType::FileNotAccessible, Some(err), path.as_path())), }; let file_name: String = String::from(path.file_name().unwrap().to_str().unwrap_or("")); // Match dir / file @@ -455,7 +470,7 @@ impl Localhost { Ok(s) => Ok(s.to_string()), Err(_) => Ok(String::new()), }, - Err(err) => Err(HostError::new(HostErrorType::ExecutionFailed, Some(err))), + Err(err) => Err(HostError::new(HostErrorType::ExecutionFailed, Some(err), self.wrkdir.as_path())), } } @@ -472,10 +487,10 @@ impl Localhost { mpex.set_mode(self.mode_to_u32(pex)); match set_permissions(path.as_path(), mpex) { Ok(_) => Ok(()), - Err(err) => Err(HostError::new(HostErrorType::FileNotAccessible, Some(err))), + Err(err) => Err(HostError::new(HostErrorType::FileNotAccessible, Some(err), path.as_path())), } } - Err(err) => Err(HostError::new(HostErrorType::FileNotAccessible, Some(err))), + Err(err) => Err(HostError::new(HostErrorType::FileNotAccessible, Some(err), path.as_path())), } } @@ -485,7 +500,7 @@ impl Localhost { pub fn open_file_read(&self, file: &Path) -> Result { let file: PathBuf = self.to_abs_path(file); if !self.file_exists(file.as_path()) { - return Err(HostError::new(HostErrorType::NoSuchFileOrDirectory, None)); + return Err(HostError::new(HostErrorType::NoSuchFileOrDirectory, None, file.as_path())); } match OpenOptions::new() .create(false) @@ -494,7 +509,7 @@ impl Localhost { .open(file.as_path()) { Ok(f) => Ok(f), - Err(err) => Err(HostError::new(HostErrorType::FileNotAccessible, Some(err))), + Err(err) => Err(HostError::new(HostErrorType::FileNotAccessible, Some(err), file.as_path())), } } @@ -511,8 +526,8 @@ impl Localhost { { Ok(f) => Ok(f), Err(err) => match self.file_exists(file.as_path()) { - true => Err(HostError::new(HostErrorType::ReadonlyFile, Some(err))), - false => Err(HostError::new(HostErrorType::FileNotAccessible, Some(err))), + true => Err(HostError::new(HostErrorType::ReadonlyFile, Some(err), file.as_path())), + false => Err(HostError::new(HostErrorType::FileNotAccessible, Some(err), file.as_path())), }, } } @@ -526,20 +541,21 @@ impl Localhost { /// ### scan_dir /// - /// Get content of the current directory as a list of fs entry (Windows) + /// Get content of the current directory as a list of fs entry pub fn scan_dir(&self, dir: &Path) -> Result, HostError> { - let entries = match std::fs::read_dir(dir) { - Ok(e) => e, - Err(err) => return Err(HostError::new(HostErrorType::DirNotAccessible, Some(err))), - }; - let mut fs_entries: Vec = Vec::new(); - for entry in entries.flatten() { - fs_entries.push(match self.stat(entry.path().as_path()) { - Ok(entry) => entry, - Err(err) => return Err(err), - }); + match std::fs::read_dir(dir) { + Ok(e) => { + let mut fs_entries: Vec = Vec::new(); + for entry in e.flatten() { + // NOTE: 0.4.1, don't fail if stat for one file fails + if let Ok(entry) = self.stat(entry.path().as_path()) { + fs_entries.push(entry); + } + } + Ok(fs_entries) + }, + Err(err) => Err(HostError::new(HostErrorType::DirNotAccessible, Some(err), dir)), } - Ok(fs_entries) } /// ### find @@ -641,9 +657,9 @@ mod tests { #[test] fn test_host_error_new() { - let error: HostError = HostError::new(HostErrorType::CouldNotCreateFile, None); - assert_eq!(error.error, HostErrorType::CouldNotCreateFile); + let error: HostError = HostError::new(HostErrorType::CouldNotCreateFile, None, Path::new("/tmp")); assert!(error.ioerr.is_none()); + assert_eq!(error.path.as_ref().unwrap(), Path::new("/tmp")); } #[test] @@ -1068,40 +1084,41 @@ mod tests { let err: HostError = HostError::new( HostErrorType::CouldNotCreateFile, Some(std::io::Error::from(std::io::ErrorKind::AddrInUse)), + Path::new("/tmp"), ); assert_eq!( format!("{}", err), - String::from("Could not create file: address in use") + String::from("Could not create file: address in use (/tmp)"), ); assert_eq!( - format!("{}", HostError::new(HostErrorType::DeleteFailed, None)), + format!("{}", HostError::from(HostErrorType::DeleteFailed)), String::from("Could not delete file") ); assert_eq!( - format!("{}", HostError::new(HostErrorType::ExecutionFailed, None)), - String::from("Could not run command") + format!("{}", HostError::from(HostErrorType::ExecutionFailed)), + String::from("Command execution failed"), ); assert_eq!( - format!("{}", HostError::new(HostErrorType::DirNotAccessible, None)), - String::from("Could not access directory") + format!("{}", HostError::from(HostErrorType::DirNotAccessible)), + String::from("Could not access directory"), ); assert_eq!( format!( "{}", - HostError::new(HostErrorType::NoSuchFileOrDirectory, None) + HostError::from(HostErrorType::NoSuchFileOrDirectory) ), String::from("No such file or directory") ); assert_eq!( - format!("{}", HostError::new(HostErrorType::ReadonlyFile, None)), + format!("{}", HostError::from(HostErrorType::ReadonlyFile)), String::from("File is readonly") ); assert_eq!( - format!("{}", HostError::new(HostErrorType::FileNotAccessible, None)), + format!("{}", HostError::from(HostErrorType::FileNotAccessible)), String::from("Could not access file") ); assert_eq!( - format!("{}", HostError::new(HostErrorType::FileAlreadyExists, None)), + format!("{}", HostError::from(HostErrorType::FileAlreadyExists)), String::from("File already exists") ); }