diff --git a/.travis.yml b/.travis.yml index 967abec..3c333e4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,6 @@ language: rust rust: - stable script: - - cargo build --verbose - - cargo test --verbose - - cargo doc + - cargo build --verbose --features filetime + - cargo test --verbose --features filetime + - cargo doc --features filetime diff --git a/Cargo.toml b/Cargo.toml index 1cfb325..4aab9e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,3 +18,4 @@ include = [ ] [dependencies] +filetime = { version = "0.2", optional = true } diff --git a/src/dir.rs b/src/dir.rs index 9024c52..6a8f930 100644 --- a/src/dir.rs +++ b/src/dir.rs @@ -4,6 +4,9 @@ use std::fs::{create_dir, create_dir_all, read_dir, remove_dir_all, Metadata}; use std::path::{Path, PathBuf}; use std::time::SystemTime; +#[cfg(feature = "filetime")] +use crate::time::{TimeOptions, copy_time}; + /// Options and flags which can be used to configure how a file will be copied or moved. #[derive(Clone)] pub struct CopyOptions { @@ -21,6 +24,9 @@ pub struct CopyOptions { /// /// Warning: Work only for copy operations! pub depth: u64, + /// File time options. + #[cfg(feature = "filetime")] + pub time_options: TimeOptions, } impl CopyOptions { @@ -43,6 +49,8 @@ impl CopyOptions { copy_inside: false, content_only: false, depth: 0, + #[cfg(feature = "filetime")] + time_options: TimeOptions::new(), } } } @@ -561,17 +569,9 @@ where } let dir_content = get_dir_content2(from, &read_options)?; - for directory in dir_content.directories { - let tmp_to = Path::new(&directory).strip_prefix(from)?; - let dir = to.join(&tmp_to); - if !dir.exists() { - if options.copy_inside { - create_all(dir, false)?; - } else { - create(dir, false)?; - } - } - } + + copy_dir_structure(from, to.as_path(), &dir_content, &options); + let mut result: u64 = 0; for file in dir_content.files { let to = to.to_path_buf(); @@ -582,6 +582,8 @@ where overwrite: options.overwrite, skip_exist: options.skip_exist, buffer_size: options.buffer_size, + #[cfg(feature = "filetime")] + time_options: options.time_options.clone(), }; let mut result_copy: Result; let mut work = true; @@ -841,17 +843,8 @@ where } let dir_content = get_dir_content2(from, &read_options)?; - for directory in dir_content.directories { - let tmp_to = Path::new(&directory).strip_prefix(from)?; - let dir = to.join(&tmp_to); - if !dir.exists() { - if options.copy_inside { - create_all(dir, false)?; - } else { - create(dir, false)?; - } - } - } + + copy_dir_structure(from, to.as_path(), &dir_content, &options); let mut result: u64 = 0; let mut info_process = TransitProcess { @@ -880,6 +873,8 @@ where overwrite: options.overwrite, skip_exist: options.skip_exist, buffer_size: options.buffer_size, + #[cfg(feature = "filetime")] + time_options: options.time_options.clone(), }; if let Some(file_name) = file_name.to_str() { @@ -1054,17 +1049,9 @@ where to.push(dir_name); } let dir_content = get_dir_content(from)?; - for directory in dir_content.directories { - let tmp_to = Path::new(&directory).strip_prefix(from)?; - let dir = to.join(&tmp_to); - if !dir.exists() { - if options.copy_inside { - create_all(dir, false)?; - } else { - create(dir, false)?; - } - } - } + + copy_dir_structure(from, to.as_path(), &dir_content, &options); + let mut result: u64 = 0; for file in dir_content.files { let to = to.to_path_buf(); @@ -1075,6 +1062,8 @@ where overwrite: options.overwrite, skip_exist: options.skip_exist, buffer_size: options.buffer_size, + #[cfg(feature = "filetime")] + time_options: options.time_options.clone(), }; let mut result_copy: Result; @@ -1178,17 +1167,8 @@ where } let dir_content = get_dir_content(from)?; - for directory in dir_content.directories { - let tmp_to = Path::new(&directory).strip_prefix(from)?; - let dir = to.join(&tmp_to); - if !dir.exists() { - if options.copy_inside { - create_all(dir, false)?; - } else { - create(dir, false)?; - } - } - } + + copy_dir_structure(from, to.as_path(), &dir_content, &options); let mut result: u64 = 0; let mut info_process = TransitProcess { @@ -1217,6 +1197,8 @@ where overwrite: options.overwrite, skip_exist: options.skip_exist, buffer_size: options.buffer_size, + #[cfg(feature = "filetime")] + time_options: options.time_options.clone(), }; if let Some(file_name) = file_name.to_str() { @@ -1345,3 +1327,21 @@ pub fn remove>(path: P) -> Result<()> { Ok(()) } } + +fn copy_dir_structure(from: &Path, to: &Path, dir_content: &DirContent, options: &CopyOptions) -> Result<()> { + for directory in &dir_content.directories { + let path_from = Path::new(directory); + let tmp_to = path_from.strip_prefix(from)?; + let dir = to.join(&tmp_to); + if !dir.exists() { + if options.copy_inside { + create_all(dir.as_path(), false)?; + } else { + create(dir.as_path(), false)?; + } + #[cfg(feature = "filetime")] + copy_time(&path_from, dir, &options.time_options); + } + } + Ok(()) +} diff --git a/src/file.rs b/src/file.rs index f9a384b..8dc8800 100644 --- a/src/file.rs +++ b/src/file.rs @@ -4,6 +4,9 @@ use std::fs::{remove_file, File}; use std::io::{Read, Write}; use std::path::Path; +#[cfg(feature = "filetime")] +use crate::time::{TimeOptions, copy_time}; + // Options and flags which can be used to configure how a file will be copied or moved. pub struct CopyOptions { /// Sets the option true for overwrite existing files. @@ -12,6 +15,9 @@ 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, + /// File time options. + #[cfg(feature = "filetime")] + pub time_options: TimeOptions, } impl CopyOptions { @@ -30,6 +36,8 @@ impl CopyOptions { overwrite: false, skip_exist: false, buffer_size: 64000, //64kb + #[cfg(feature = "filetime")] + time_options: TimeOptions::new(), } } } @@ -106,7 +114,11 @@ where } } - Ok(std::fs::copy(from, to)?) + let result = std::fs::copy(from, to.as_ref())?; + #[cfg(feature = "filetime")] + copy_time(from, to, &options.time_options); + + Ok(result) } /// Copies the contents of one file to another file with information about progress. @@ -180,7 +192,7 @@ where let file_size = file_from.metadata()?.len(); let mut copied_bytes: u64 = 0; - let mut file_to = File::create(to)?; + let mut file_to = File::create(to.as_ref())?; while !buf.is_empty() { match file_from.read(&mut buf) { Ok(0) => break, @@ -200,6 +212,8 @@ where Err(e) => return Err(::std::convert::From::from(e)), } } + #[cfg(feature = "filetime")] + copy_time(from, to, &options.time_options); Ok(file_size) } diff --git a/src/lib.rs b/src/lib.rs index 118643a..622dd41 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,12 @@ macro_rules! err { /// The error type for fs_extra operations on files and directories. pub mod error; + +/// This module includes an additional method for working with file and +/// directory modification times and access times. +#[cfg(feature = "filetime")] +pub mod time; + /// This module includes additional methods for working with files. /// /// One of the distinguishing features is receipt information @@ -354,6 +360,8 @@ where overwrite: options.overwrite, skip_exist: options.skip_exist, buffer_size: options.buffer_size, + #[cfg(feature = "filetime")] + time_options: options.time_options.clone(), }; if let Some(file_name) = item.file_name() { @@ -541,6 +549,8 @@ where overwrite: options.overwrite, skip_exist: options.skip_exist, buffer_size: options.buffer_size, + #[cfg(feature = "filetime")] + time_options: options.time_options.clone(), }; if let Some(file_name) = item.file_name() { @@ -666,6 +676,8 @@ where overwrite: options.overwrite, skip_exist: options.skip_exist, buffer_size: options.buffer_size, + #[cfg(feature = "filetime")] + time_options: options.time_options.clone(), }; if let Some(file_name) = item.file_name() { diff --git a/src/time.rs b/src/time.rs new file mode 100644 index 0000000..eee15e0 --- /dev/null +++ b/src/time.rs @@ -0,0 +1,50 @@ +use crate::error::*; +use std::path::Path; + +use filetime::{set_file_atime, set_file_mtime, FileTime}; + +/// Flags which can be used to configure how file and directory times will be +/// assigned. +#[derive(Clone)] +pub struct TimeOptions { + /// Keep the same modification time. + pub retain_modification_time: bool, + /// Keep the same access time. + pub retain_access_time: bool, +} + +impl TimeOptions { + /// Initialize struct TimeOptions with default value. + pub fn new() -> Self { + TimeOptions { + retain_modification_time: false, + retain_access_time: false, + } + } +} + +/// Assign time attributes for `to` same as in `from`. +pub fn copy_time(from: P, to: Q, options: &TimeOptions) -> Result<()> +where + P: AsRef, + Q: AsRef, +{ + if options.retain_modification_time || options.retain_access_time { + match from.as_ref().metadata() { + Ok(metadata) => { + let mtime = FileTime::from_last_modification_time(&metadata); + let atime = FileTime::from_last_access_time(&metadata); + if options.retain_modification_time { + set_file_mtime(to.as_ref(), mtime); + } + if options.retain_access_time { + set_file_atime(to.as_ref(), atime); + } + } + Err(_) => { + err!("Could not read metadata", ErrorKind::Other); + } + } + } + Ok(()) +} diff --git a/tests/dir.rs b/tests/dir.rs index 0f3c63e..60d6880 100644 --- a/tests/dir.rs +++ b/tests/dir.rs @@ -3,6 +3,7 @@ use std::fs::read_dir; use std::path::{Path, PathBuf}; use std::sync::mpsc::{self, TryRecvError}; use std::thread; +use std::time::SystemTime; extern crate fs_extra; use fs_extra::dir::*; @@ -1762,34 +1763,51 @@ where true } -#[test] -fn it_move_work() { - let mut path_from = PathBuf::from(TEST_FOLDER); - let test_name = "sub"; - path_from.push("it_move_work"); - let mut path_to = path_from.clone(); - path_to.push("out"); - path_from.push(&test_name); - - create_all(&path_from, true).unwrap(); - assert!(path_from.exists()); - create_all(&path_to, true).unwrap(); - assert!(path_to.exists()); +macro_rules! create_all { + ($($var:ident = $fragment_first:expr $(=> $fragment:expr)*),* $(,)?) => { + $( + let mut path = PathBuf::from($fragment_first); + $( + path.push($fragment); + )* + create_all(&path, true).unwrap(); + assert!(path.exists()); + $var = path; + )* + }; +} - let mut file1_path = path_from.clone(); - file1_path.push("test1.txt"); - let content1 = "content1"; - fs_extra::file::write_all(&file1_path, &content1).unwrap(); - assert!(file1_path.exists()); +macro_rules! write_all { + ($([$fragment_first:expr $(=> $fragment:expr)* ] = $content:expr),* $(,)?) => { + $( + let mut path = PathBuf::from($fragment_first); + let mut _iter_num = 1; + $( + if _iter_num >= 2 { + create(&path, true).unwrap(); + } + path.push($fragment); + _iter_num += 1; + )* + let content = $content; + fs_extra::file::write_all(&path, &content).unwrap(); + assert!(path.exists()); + )* + }; +} - let mut sub_dir_path = path_from.clone(); - sub_dir_path.push("sub"); - create(&sub_dir_path, true).unwrap(); - let mut file2_path = sub_dir_path.clone(); - file2_path.push("test2.txt"); - let content2 = "content2"; - fs_extra::file::write_all(&file2_path, &content2).unwrap(); - assert!(file2_path.exists()); +#[test] +fn it_move_work() { + let path_from; + let path_to; + create_all!( + path_from = TEST_FOLDER => "it_move_work" => "sub", + path_to = TEST_FOLDER => "it_move_work" => "out", + ); + write_all!( + [ path_from.clone() => "test1.txt" ] = "content1", + [ path_from.clone() => "sub" => "test2.txt" ] = "content2", + ); let options = CopyOptions::new(); let result = move_dir(&path_from, &path_to, &options).unwrap(); @@ -4786,3 +4804,74 @@ fn it_move_with_progress_content_only_option() { _ => {} } } + +#[cfg(feature = "filetime")] +fn atime(path: &Path) -> Option { + match path.metadata() { + Ok(metadata) => { + metadata.accessed().ok() + } + Err(err) => { + panic!(err); + } + } +} + +#[cfg(feature = "filetime")] +fn mtime(path: &Path) -> Option { + match path.metadata() { + Ok(metadata) => { + metadata.modified().ok() + } + Err(err) => { + panic!("{:?}", err); + } + } +} + +#[cfg(feature = "filetime")] +#[test] +fn it_move_work_with_same_mtime_and_atime() { + let path_from; + let path_to; + create_all!( + path_from = TEST_FOLDER => "it_move_work_with_same_mtime_and_atime" => "sub", + path_to = TEST_FOLDER => "it_move_work_with_same_mtime_and_atime" => "out", + ); + write_all!( + [ path_from.clone() => "test1.txt" ] = "content1", + [ path_from.clone() => "sub" => "test2.txt" ] = "content2", + ); + + let mut file1_path = path_from.join("test1.txt"); + let mut file2_path = path_from.join("sub").join("test2.txt"); + let mut sub_path = path_from.join("sub"); + let expected_file1_atime = atime(file1_path.as_path()); + let expected_file1_mtime = mtime(file1_path.as_path()); + let expected_file2_atime = atime(file2_path.as_path()); + let expected_file2_mtime = mtime(file2_path.as_path()); + let expected_sub_atime = atime(sub_path.as_path()); + let expected_sub_mtime = mtime(sub_path.as_path()); + + let mut options = CopyOptions::new(); + options.time_options.retain_modification_time = true; + options.time_options.retain_access_time = true; + let result = move_dir(&path_from, &path_to, &options).unwrap(); + + assert_eq!(16, result); + assert!(path_to.exists()); + assert!(!path_from.exists()); + + let mut file1_path = path_to.join("sub").join("test1.txt"); + assert!(file1_path.exists()); + let mut file2_path = path_to.join("sub").join("sub").join("test2.txt"); + assert!(file2_path.exists()); + let mut sub_path = path_to.join("sub").join("sub"); + assert!(sub_path.exists()); + assert_eq!(mtime(file1_path.as_path()), expected_file1_mtime); + assert_eq!(atime(file1_path.as_path()), expected_file1_atime); + assert_eq!(mtime(file2_path.as_path()), expected_file2_mtime); + assert_eq!(atime(file2_path.as_path()), expected_file2_atime); + assert_eq!(mtime(sub_path.as_path()), expected_sub_mtime); + assert_eq!(atime(sub_path.as_path()), expected_sub_atime); +}