diff --git a/src/dir.rs b/src/dir.rs index a5d1112..7badb67 100644 --- a/src/dir.rs +++ b/src/dir.rs @@ -619,6 +619,7 @@ where overwrite: options.overwrite, skip_exist: options.skip_exist, buffer_size: options.buffer_size, + follow: false, }; let mut result_copy: Result; let mut work = true; @@ -726,7 +727,8 @@ where } let item = item.unwrap().to_string(); - if path.as_ref().is_dir() { + let meta = path.as_ref().symlink_metadata()?; + if meta.is_dir() { dir_size = path.as_ref().metadata()?.len(); directories.push(item); if depth == 0 || depth > 1 { @@ -749,7 +751,7 @@ where } } } else { - dir_size = path.as_ref().metadata()?.len(); + dir_size = meta.len(); files.push(item); } Ok(DirContent { @@ -931,6 +933,7 @@ where overwrite: options.overwrite, skip_exist: options.skip_exist, buffer_size: options.buffer_size, + follow: false, }; if let Some(file_name) = file_name.to_str() { @@ -940,7 +943,7 @@ where } info_process.file_bytes_copied = 0; - info_process.file_total_bytes = Path::new(&file).metadata()?.len(); + info_process.file_total_bytes = Path::new(&file).symlink_metadata()?.len(); let mut result_copy: Result; let mut work = true; @@ -1126,6 +1129,7 @@ where overwrite: options.overwrite, skip_exist: options.skip_exist, buffer_size: options.buffer_size, + follow: false, }; let mut result_copy: Result; @@ -1146,6 +1150,7 @@ where } } } + if is_remove { remove(from)?; } @@ -1268,6 +1273,7 @@ where overwrite: options.overwrite, skip_exist: options.skip_exist, buffer_size: options.buffer_size, + follow: false, }; if let Some(file_name) = file_name.to_str() { @@ -1277,7 +1283,7 @@ where } info_process.file_bytes_copied = 0; - info_process.file_total_bytes = Path::new(&file).metadata()?.len(); + info_process.file_total_bytes = Path::new(&file).symlink_metadata()?.len(); let mut result_copy: Result; let mut work = true; diff --git a/src/file.rs b/src/file.rs index 2b62576..f21eee0 100644 --- a/src/file.rs +++ b/src/file.rs @@ -1,7 +1,13 @@ use crate::error::{Error, ErrorKind, Result}; use std; -use std::fs::{remove_file, File}; +use std::fs::{read_link, remove_file, File}; use std::io::{Read, Write}; +#[cfg(any(unix, target_os = "redox"))] +use std::os::unix::fs::symlink; +#[cfg(target_os = "wasi")] +use std::os::wasi::fs::symlink_path as symlink; +#[cfg(windows)] +use std::os::windows::fs::{symlink_dir, symlink_file, FileTypeExt}; use std::path::Path; // Options and flags which can be used to configure how a file will be copied or moved. @@ -12,6 +18,8 @@ pub struct CopyOptions { pub skip_exist: bool, /// Sets buffer size for copy/move work only with receipt information about process work. pub buffer_size: usize, + /// Follows the last symbolic link in the path + pub follow: bool, } impl CopyOptions { @@ -30,6 +38,7 @@ impl CopyOptions { overwrite: false, skip_exist: false, buffer_size: 64000, //64kb + follow: true, } } @@ -94,34 +103,43 @@ where Q: AsRef, { let from = from.as_ref(); - if !from.exists() { - if let Some(msg) = from.to_str() { - let msg = format!("Path \"{}\" does not exist or you don't have access!", msg); - err!(&msg, ErrorKind::NotFound); + let to = to.as_ref(); + + if !options.overwrite && to.symlink_metadata().is_ok() { + if !options.skip_exist { + err!( + &format!("Path \"{}\" exists", to.display()), + ErrorKind::AlreadyExists + ); + } else { + return Ok(0); } - err!( - "Path does not exist or you don't have access!", - ErrorKind::NotFound - ); } - if !from.is_file() { - if let Some(msg) = from.to_str() { - let msg = format!("Path \"{}\" is not a file!", msg); - err!(&msg, ErrorKind::InvalidFile); + if !options.follow && from.is_symlink() { + #[cfg(any(unix, target_os = "wasi", target_os = "redox"))] + symlink(read_link(from)?, to)?; + #[cfg(windows)] + if from.symlink_metadata()?.file_type().is_symlink_dir() { + symlink_dir(read_link(from)?, to)?; + } else { + symlink_file(read_link(from)?, to)?; } - err!("Path is not a file!", ErrorKind::InvalidFile); + return Ok(to.as_os_str().len() as u64); } - if !options.overwrite && to.as_ref().exists() { - if options.skip_exist { - return Ok(0); - } + if !from.try_exists()? { + err!( + &format!("Path \"{}\" doesn't exist!", from.display()), + ErrorKind::NotFound + ); + } - if let Some(msg) = to.as_ref().to_str() { - let msg = format!("Path \"{}\" exists", msg); - err!(&msg, ErrorKind::AlreadyExists); - } + if !from.is_file() { + err!( + &format!("Path \"{}\" is not a file!", from.display()), + ErrorKind::InvalidFile + ); } Ok(std::fs::copy(from, to)?) @@ -164,35 +182,45 @@ where F: FnMut(TransitProcess), { let from = from.as_ref(); - if !from.exists() { - if let Some(msg) = from.to_str() { - let msg = format!("Path \"{}\" does not exist or you don't have access!", msg); - err!(&msg, ErrorKind::NotFound); + let to = to.as_ref(); + + if !options.overwrite && (to.try_exists()? || to.symlink_metadata().is_ok()) { + if !options.skip_exist { + err!( + &format!("Path \"{}\" exists", to.display()), + ErrorKind::AlreadyExists + ); + } else { + return Ok(0); + } + } + + if !options.follow && from.is_symlink() { + #[cfg(any(unix, target_os = "wasi", target_os = "redox"))] + symlink(read_link(from)?, to)?; + #[cfg(windows)] + if from.symlink_metadata()?.file_type().is_symlink_dir() { + symlink_dir(read_link(from)?, to)?; + } else { + symlink_file(read_link(from)?, to)?; } + return Ok(to.as_os_str().len() as u64); + } + + if !from.try_exists()? { err!( - "Path does not exist or you don't have access!", + &format!("Path \"{}\" doesn't exist!", from.display()), ErrorKind::NotFound ); } if !from.is_file() { - if let Some(msg) = from.to_str() { - let msg = format!("Path \"{}\" is not a file!", msg); - err!(&msg, ErrorKind::InvalidFile); - } - err!("Path is not a file!", ErrorKind::InvalidFile); + err!( + &format!("Path \"{}\" is not a file!", from.display()), + ErrorKind::InvalidFile + ); } - if !options.overwrite && to.as_ref().exists() { - if options.skip_exist { - return Ok(0); - } - - if let Some(msg) = to.as_ref().to_str() { - let msg = format!("Path \"{}\" exists", msg); - err!(&msg, ErrorKind::AlreadyExists); - } - } let mut file_from = File::open(from)?; let mut buf = vec![0; options.buffer_size]; let file_size = file_from.metadata()?.len(); diff --git a/src/lib.rs b/src/lib.rs index 118643a..63a418e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +#![cfg_attr(target_os = "wasi", feature(wasi_ext))] + macro_rules! err { ($text:expr, $kind:expr) => { return Err(Error::new($kind, $text)) @@ -354,6 +356,7 @@ where overwrite: options.overwrite, skip_exist: options.skip_exist, buffer_size: options.buffer_size, + follow: false, }; if let Some(file_name) = item.file_name() { @@ -541,6 +544,7 @@ where overwrite: options.overwrite, skip_exist: options.skip_exist, buffer_size: options.buffer_size, + follow: false, }; if let Some(file_name) = item.file_name() { @@ -666,6 +670,7 @@ where overwrite: options.overwrite, skip_exist: options.skip_exist, buffer_size: options.buffer_size, + follow: false, }; if let Some(file_name) = item.file_name() { diff --git a/tests/dir.rs b/tests/dir.rs index 2647066..6b05837 100644 --- a/tests/dir.rs +++ b/tests/dir.rs @@ -18,6 +18,16 @@ where content1 == content2 } +fn symlinks_eq(symlink1: P, symlink2: Q) -> bool +where + P: AsRef, + Q: AsRef, +{ + let target1 = fs::read_link(symlink1).unwrap(); + let target2 = fs::read_link(symlink2).unwrap(); + target1 == target2 +} + fn compare_dir(path_from: P, path_to: Q) -> bool where P: AsRef, @@ -28,7 +38,7 @@ where None => panic!("Invalid folder from"), Some(dir_name) => { path_to.push(dir_name.as_os_str()); - if !path_to.exists() { + if !path_to.exists() && !path_to.is_symlink() { return false; } } @@ -47,10 +57,16 @@ where None => panic!("No file name"), Some(file_name) => { path_to.push(file_name); - if !path_to.exists() { - return false; - } else if !files_eq(&path, path_to.clone()) { - return false; + if path_to.is_symlink() { + if !symlinks_eq(&path, &path_to) { + return false; + } + } else if path_to.is_file() { + if !path_to.exists() { + return false; + } else if !files_eq(&path, path_to.clone()) { + return false; + } } } } @@ -70,7 +86,7 @@ fn get_dir_size() -> u64 { .len() } -#[cfg(unix)] +#[cfg(any(unix, target_os = "wasi", target_os = "redox"))] fn create_file_symlink, Q: AsRef>( original: P, link: Q, @@ -4822,3 +4838,79 @@ fn it_move_with_progress_content_only_option() { _ => {} } } + +#[test] +fn it_move_with_symlinks() { + let copy_options = &CopyOptions { + copy_inside: true, + ..CopyOptions::new() + }; + let test_dir = Path::new(TEST_FOLDER).join("it_move_with_symlinks"); + let path_from = test_dir.clone().join("dir"); + let path_to1 = test_dir.clone().join("dir_cpy1"); + let path_to2 = test_dir.clone().join("dir_cpy2"); + + let test_file = Path::new("file"); + let test_link = Path::new("link"); + let test_dir1 = Path::new("dir_in"); + let test_link1 = test_dir1.clone().join("link1"); + + let _ = remove(test_dir); + create_all(&path_from.join(&test_dir1), true).unwrap(); + fs_extra::file::write_all(&path_from.join(&test_file), "test").unwrap(); + + create_file_symlink(&test_file, &path_from.join(&test_link)).unwrap(); + create_file_symlink( + &Path::new("..").join(test_file), + &path_from.join(&test_link1), + ) + .unwrap(); + + copy(&path_from, &path_to1.join("dir"), ©_options).unwrap(); + assert!(compare_dir(&path_from, &path_to1)); + + move_dir(&path_to1.join("dir"), &path_to2.join("dir"), ©_options).unwrap(); + assert!(compare_dir(&path_from, &path_to2)); +} + +#[test] +fn it_move_with_symlinks_progress() { + let copy_options = &CopyOptions { + copy_inside: true, + ..CopyOptions::new() + }; + let test_dir = Path::new(TEST_FOLDER).join("it_move_with_symlinks_progress"); + let path_from = test_dir.clone().join("dir"); + let path_to1 = test_dir.clone().join("dir_cpy1"); + let path_to2 = test_dir.clone().join("dir_cpy2"); + + let test_file = Path::new("file"); + let test_link = Path::new("link"); + let test_dir1 = Path::new("dir_in"); + let test_link1 = test_dir1.clone().join("link1"); + + let func_test = |_process_info: TransitProcess| TransitProcessResult::ContinueOrAbort; + + let _ = remove(test_dir); + create_all(&path_from.join(&test_dir1), true).unwrap(); + fs_extra::file::write_all(&path_from.join(&test_file), "test").unwrap(); + + create_file_symlink(&test_file, &path_from.join(&test_link)).unwrap(); + create_file_symlink( + &Path::new("..").join(test_file), + &path_from.join(&test_link1), + ) + .unwrap(); + + copy_with_progress(&path_from, &path_to1.join("dir"), ©_options, &func_test).unwrap(); + assert!(compare_dir(&path_from, &path_to1)); + + move_dir_with_progress( + &path_to1.join("dir"), + &path_to2.join("dir"), + ©_options, + &func_test, + ) + .unwrap(); + assert!(compare_dir(&path_from, &path_to2)); +} diff --git a/tests/file.rs b/tests/file.rs index 67840a5..cac47f1 100644 --- a/tests/file.rs +++ b/tests/file.rs @@ -9,6 +9,22 @@ use fs_extra::file::*; const TEST_FOLDER: &'static str = "./tests/temp/file"; +#[cfg(unix)] +fn create_file_symlink, Q: AsRef>( + original: P, + link: Q, +) -> std::io::Result<()> { + std::os::unix::fs::symlink(original.as_ref(), link.as_ref()) +} + +#[cfg(windows)] +fn create_file_symlink, Q: AsRef>( + original: P, + link: Q, +) -> std::io::Result<()> { + std::os::windows::fs::symlink_file(original.as_ref(), link.as_ref()) +} + fn files_eq(file1: P, file2: Q) -> Result where P: AsRef, @@ -167,11 +183,7 @@ fn it_copy_source_not_exist() { Ok(_) => panic!("should be error"), Err(err) => match err.kind { ErrorKind::NotFound => { - let wrong_path = format!( - "Path \"{}\" does not exist or you don't have \ - access!", - test_file.to_str().unwrap() - ); + let wrong_path = format!("Path \"{}\" doesn't exist!", test_file.to_str().unwrap()); assert_eq!(wrong_path, err.to_string()); () } @@ -429,11 +441,7 @@ fn it_copy_with_progress_source_not_exist() { Ok(_) => panic!("should be error"), Err(err) => match err.kind { ErrorKind::NotFound => { - let wrong_path = format!( - "Path \"{}\" does not exist or you don't have \ - access!", - test_file.to_str().unwrap() - ); + let wrong_path = format!("Path \"{}\" doesn't exist!", test_file.to_str().unwrap()); assert_eq!(wrong_path, err.to_string()); () @@ -642,11 +650,7 @@ fn it_move_source_not_exist() { Ok(_) => panic!("should be error"), Err(err) => match err.kind { ErrorKind::NotFound => { - let wrong_path = format!( - "Path \"{}\" does not exist or you don't have \ - access!", - test_file.to_str().unwrap() - ); + let wrong_path = format!("Path \"{}\" doesn't exist!", test_file.to_str().unwrap()); assert_eq!(wrong_path, err.to_string()); () @@ -896,11 +900,7 @@ fn it_move_with_progress_source_not_exist() { Ok(_) => panic!("should be error"), Err(err) => match err.kind { ErrorKind::NotFound => { - let wrong_path = format!( - "Path \"{}\" does not exist or you don't have \ - access!", - test_file.to_str().unwrap() - ); + let wrong_path = format!("Path \"{}\" doesn't exist!", test_file.to_str().unwrap()); assert_eq!(wrong_path, err.to_string()); () @@ -1034,3 +1034,64 @@ fn it_move_with_progress_exist_overwrite_and_skip_exist() { Err(err) => panic!(err.to_string()), } } + +#[test] +#[cfg(any(unix, windows, target_os = "redox", target_os = "wasi"))] +fn it_copy_not_overwrite_broken_symlink() { + let mut test_file = PathBuf::from(TEST_FOLDER); + test_file.push("it_copy_not_overwrite_broken_symlink"); + let mut test_link = test_file.clone(); + let mut test_target = test_file.clone(); + + test_file.push("file"); + test_link.push("link"); + test_target.push("nothing"); + + fs_extra::dir::create_all(&test_link.parent().unwrap(), true).unwrap(); + fs_extra::dir::create_all(&test_file.parent().unwrap(), true).unwrap(); + + write_all(&test_file, "test").unwrap(); + create_file_symlink(test_target, &test_link).unwrap(); + + let options = CopyOptions::new(); + + match copy(&test_file, &test_link, &options) { + Ok(_) => panic!("should be error"), + Err(err) => match err.kind { + ErrorKind::AlreadyExists => {} + _ => panic!("wrong error: {:?}", err.kind), + }, + } +} + +#[test] +#[cfg(any(unix, windows, target_os = "redox", target_os = "wasi"))] +fn it_copy_symlink_not_follow() { + let mut test_file = PathBuf::from(TEST_FOLDER); + test_file.push("it_copy_symlink_not_follow"); + let mut test_link = test_file.clone(); + let mut test_link_to = test_file.clone(); + + test_file.push("file"); + test_link.push("link"); + test_link_to.push("link_copy"); + + fs_extra::dir::create_all(&test_link.parent().unwrap(), true).unwrap(); + fs_extra::dir::create_all(&test_file.parent().unwrap(), true).unwrap(); + + write_all(&test_file, "test").unwrap(); + create_file_symlink(&test_file, &test_link).unwrap(); + + let options = CopyOptions { + follow: false, + ..CopyOptions::new() + }; + + match copy(&test_link, &test_link_to, &options) { + Ok(_) => { + assert!(test_link_to.is_symlink()); + assert_eq!(std::fs::read_link(test_link_to).unwrap(), test_file); + } + Err(_) => panic!("Should not be error"), + } +}