Added path to HostError; scan_dir won't fail if it is not possible to stat an entry

This commit is contained in:
veeso 2021-04-03 16:21:37 +02:00
parent 66068ec73c
commit af678802bb
4 changed files with 147 additions and 95 deletions

View file

@ -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

21
Cargo.lock generated
View file

@ -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"

View file

@ -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" }

View file

@ -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<std::io::Error>,
ioerr: Option<std::io::Error>,
path: Option<PathBuf>,
}
impl HostError {
/// ### new
///
/// Instantiates a new HostError
pub(crate) fn new(error: HostErrorType, errno: Option<std::io::Error>) -> HostError {
pub(crate) fn new(error: HostErrorType, errno: Option<std::io::Error>, p: &Path) -> Self {
HostError {
error,
ioerr: errno,
path: Some(p.to_path_buf())
}
}
}
impl From<HostErrorType> 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<File, HostError> {
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<Vec<FsEntry>, 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<FsEntry> = 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<FsEntry> = 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")
);
}