use std::{
env,
path::{Path, PathBuf},
};
use anyhow::bail;
use crate::config::transfer::command::DestinationMode;
pub fn resolve_scp_path(remote_path: &Path) -> anyhow::Result<PathBuf> {
if remote_path.starts_with("~") || remote_path.as_os_str().is_empty() {
let components: Vec<&str> = remote_path
.iter()
.map(|os_str| os_str.to_str().unwrap())
.collect();
let mut resolved_path = PathBuf::from(resolve_home()?);
for component in components.iter().skip(1) {
resolved_path.push(component);
}
return Ok(resolved_path);
}
Ok(remote_path.into())
}
#[cfg(not(target_os = "windows"))]
fn resolve_home() -> Result<String, env::VarError> {
env::var("HOME")
}
#[cfg(target_os = "windows")]
fn resolve_home() -> Result<String, env::VarError> {
match env::var("HOME") {
Ok(h) => Ok(h),
Err(_) => env::var("USERPROFILE"),
}
}
pub fn is_root<P: AsRef<Path>>(path: P) -> bool {
let path = path.as_ref();
path.parent().is_none() && path.has_root()
}
pub fn validate_remote_path(mode: &DestinationMode, remote_path: &Path) -> anyhow::Result<PathBuf> {
tracing::trace!("Validationg path: {remote_path:?} in {mode}");
let resolved_path = resolve_scp_path(remote_path)?;
tracing::trace!("Resolved {remote_path:?} -> {resolved_path:?}");
if is_root(&resolved_path) {
return Ok(resolved_path);
}
if !resolved_path.is_absolute() {
bail!(
"Cannot resolve '{}' to an absolute path",
remote_path.to_string_lossy()
);
}
match mode {
DestinationMode::SingleFile => {
if resolved_path.parent().is_some_and(|p| p.exists()) {
Ok(resolved_path)
} else {
bail!(
"'{}' invalid path, parent directory has to be an existent directory",
remote_path.to_string_lossy()
)
}
}
DestinationMode::MultipleFiles => {
if resolved_path.is_dir() {
Ok(resolved_path)
} else {
bail!("transferring multiple files requires an existent destination directory")
}
}
DestinationMode::RecusiveDirectory => {
if resolved_path.is_dir()
|| (!resolved_path.is_file()
&& resolved_path.extension().is_none()
&& resolved_path.parent().is_some_and(|p| p.exists()))
{
Ok(resolved_path)
} else {
bail!("transferring a directory requires a destination directory")
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
use std::fs::File;
use strum::IntoEnumIterator;
use temp_dir::TempDir;
use testresult::TestResult;
const DIAG_IS_FILE_FORMAT: &str = "Destination has file-format";
const DIAG_IS_FILE_EXISTS: &str = "Destination exists and is file";
const DIAG_HELP_MULTI_F_DEST_DIR_REQ: &str =
"transferring multiple files requires a destination directory";
const DIAG_HELP_DIR_DEST_DIR_REQ: &str =
"transferring a directory requires a destination directory";
#[cfg(not(target_os = "windows"))]
#[test]
fn test_resolve_scp_path_with_tilde_unix() -> TestResult {
let home_dir = env::var("HOME")?;
let path = PathBuf::from("~/test_dir");
let expected_path = PathBuf::from(&home_dir).join("test_dir");
assert_eq!(resolve_scp_path(&path)?, expected_path);
Ok(())
}
#[cfg(not(target_os = "windows"))]
#[test]
fn test_resolve_scp_path_empty_str() -> TestResult {
let home_dir = env::var("HOME")?;
let path = PathBuf::from("");
let expected_path = PathBuf::from(&home_dir).join("");
assert_eq!(resolve_scp_path(&path)?, expected_path);
Ok(())
}
#[cfg(target_os = "windows")]
#[test]
fn test_resolve_scp_path_with_tilde_windows() -> TestResult {
let user_profile = env::var("USERPROFILE")?;
let path = PathBuf::from("~\\test_dir");
let expected_path = PathBuf::from(&user_profile).join("test_dir");
assert_eq!(resolve_scp_path(&path)?, expected_path);
Ok(())
}
#[cfg(target_os = "windows")]
#[test]
fn test_resolve_scp_path_with_empty_str() -> TestResult {
let user_profile = env::var("USERPROFILE")?;
let path = PathBuf::from("");
let expected_path = PathBuf::from(&user_profile).join("");
assert_eq!(resolve_scp_path(&path)?, expected_path);
Ok(())
}
#[test]
fn test_is_remote_path_valid_with_unix_home_no_file_extension_valid() {
let path = PathBuf::from("~/non_existing");
for mode in DestinationMode::iter() {
match mode {
DestinationMode::SingleFile => assert!(validate_remote_path(&mode, &path).is_ok()),
DestinationMode::MultipleFiles => {
assert!(
validate_remote_path(&mode, &path).is_err(),
"Error: Path doesn't exist, {DIAG_HELP_MULTI_F_DEST_DIR_REQ}"
)
}
DestinationMode::RecusiveDirectory => {
assert!(validate_remote_path(&mode, &path).is_ok())
}
}
}
}
#[test]
fn test_is_remote_path_valid_with_existing_directory_valid() -> TestResult {
let dir = TempDir::new()?;
let path = dir.path();
assert!(!path.ends_with("/"));
for mode in DestinationMode::iter() {
match mode {
DestinationMode::SingleFile => assert!(validate_remote_path(&mode, path).is_ok()),
DestinationMode::MultipleFiles => {
assert!(validate_remote_path(&mode, path).is_ok())
}
DestinationMode::RecusiveDirectory => {
assert!(validate_remote_path(&mode, path).is_ok())
}
}
}
Ok(())
}
#[test]
fn test_is_remote_path_valid_with_existing_directory_trailing_slash() -> TestResult {
let dir = TempDir::new()?;
let mut dir_path = dir.path().to_str().unwrap().to_owned();
dir_path.push('/');
let path = PathBuf::from(dir_path);
for mode in DestinationMode::iter() {
match mode {
DestinationMode::SingleFile => assert!(validate_remote_path(&mode, &path).is_ok()),
DestinationMode::MultipleFiles => {
assert!(validate_remote_path(&mode, &path).is_ok())
}
DestinationMode::RecusiveDirectory => {
assert!(validate_remote_path(&mode, &path).is_ok())
}
}
}
Ok(())
}
#[test]
fn test_is_remote_path_valid_with_existing_file() -> TestResult {
let dir = TempDir::new()?;
let path = dir.child("file.txt");
File::create(&path)?;
for mode in DestinationMode::iter() {
match mode {
DestinationMode::SingleFile => assert!(validate_remote_path(&mode, &path).is_ok()),
DestinationMode::MultipleFiles => {
assert!(
validate_remote_path(&mode, &path).is_err(),
"Error: {DIAG_IS_FILE_EXISTS}, {DIAG_HELP_MULTI_F_DEST_DIR_REQ}"
)
}
DestinationMode::RecusiveDirectory => {
assert!(
validate_remote_path(&mode, &path).is_err(),
"Error: {DIAG_IS_FILE_EXISTS}, {DIAG_HELP_DIR_DEST_DIR_REQ}"
)
}
}
}
Ok(())
}
#[test]
fn test_is_remote_path_valid_with_existing_directory_but_non_existent_file() -> TestResult {
let dir = TempDir::new()?;
let path = dir.child("doesnt_exist.txt");
for mode in DestinationMode::iter() {
match mode {
DestinationMode::SingleFile => assert!(validate_remote_path(&mode, &path).is_ok()),
DestinationMode::MultipleFiles => {
assert!(
validate_remote_path(&mode, &path).is_err(),
"Error: {DIAG_IS_FILE_FORMAT}, {DIAG_HELP_MULTI_F_DEST_DIR_REQ}"
)
}
DestinationMode::RecusiveDirectory => {
assert!(
validate_remote_path(&mode, &path).is_err(),
"Error: {DIAG_IS_FILE_FORMAT}, {DIAG_HELP_DIR_DEST_DIR_REQ}"
)
}
}
}
Ok(())
}
#[test]
fn test_is_remote_path_valid_with_non_absolute_path() {
let path = PathBuf::from(
"dsj764j7654j96h6ybvjihsbd4747cbds77r44fdsf9e4b4h6f0qxlmusghd7ahndcjsahf2sad",
); for mode in DestinationMode::iter() {
match mode {
DestinationMode::SingleFile => assert!(
validate_remote_path(&mode, &path).is_err(),
"Err: Cannot resolve to absolute path"
),
DestinationMode::MultipleFiles => {
assert!(
validate_remote_path(&mode, &path).is_err(),
"Err: Cannot resolve to absolute path"
)
}
DestinationMode::RecusiveDirectory => {
assert!(
validate_remote_path(&mode, &path).is_err(),
"Err: Cannot resolve to absolute path"
)
}
}
}
}
#[test]
fn test_validate_remote_path_with_root_issue37() {
let path = PathBuf::from("/");
for mode in DestinationMode::iter() {
match mode {
DestinationMode::SingleFile => assert!(validate_remote_path(&mode, &path).is_ok()),
DestinationMode::MultipleFiles => {
assert!(validate_remote_path(&mode, &path).is_ok())
}
DestinationMode::RecusiveDirectory => {
assert!(validate_remote_path(&mode, &path).is_ok())
}
}
}
}
}