use filetime::FileTime;
use std::error::Error;
use std::fs;
use std::io;
use std::path::Path;
use std::time::UNIX_EPOCH;
use std::collections::HashMap;
use clap::{Arg, ArgMatches, Command, crate_version};
use walkdir::{DirEntry, WalkDir};
pub struct Config {
pub source: String,
pub target: String,
pub extensions: Vec<String>,
}
impl Config {
pub fn new(matches: ArgMatches) -> Config {
let source = matches.get_one::<String>("source-dir").unwrap().to_string();
let target = matches.get_one::<String>("target-dir").unwrap().to_string();
let extensions: Vec<String> = matches.get_one::<String>("file-extensions").unwrap().to_string().split(',').map(|s| s.to_string()).collect();
Config { source, target, extensions }
}
}
#[derive(Debug, PartialEq, Eq)]
struct File {
file_name: String,
size: u64,
ctime: u64,
mtime: u64,
path: String,
}
pub fn build_clap() -> ArgMatches {
Command::new("diffcopy")
.author("Rolf Speer <rolf.speer@gmail.com>")
.about("Copy modified files from sub directories into one target directory")
.arg(
Arg::new("source-dir")
.value_name("source-dir")
.help("source directory for searching for the changed files (incl. subdirectories)")
.index(1)
.required(true),
)
.arg(
Arg::new("target-dir")
.value_name("target-dir")
.help("target directory for storing the copied files")
.index(2)
.required(true),
)
.arg(
Arg::new("file-extensions")
.value_name("file-extensions")
.help("all file extensions to be searched for")
.index(3)
.required(true)
)
.version(crate_version!())
.get_matches()
}
pub fn run(config: &Config) -> Result<(), Box<dyn Error>> {
println!("source directory: {}", config.source);
println!("target directory: {}", config.target);
let extensions_string = config.extensions.join("|");
println!("extensions : {}", extensions_string);
println!("- source_files");
let mut source_files = get_files(&config.source, &config.extensions)?;
let mut highest_modification_times: HashMap<String, u64> = HashMap::new();
for entry in &source_files {
let filename = entry.file_name.clone();
let mtime = entry.mtime;
if let Some(current_time) = highest_modification_times.get_mut(&filename) {
if mtime > *current_time {
*current_time = mtime;
}
} else {
highest_modification_times.insert(filename.clone(), mtime);
}
}
source_files.retain(|entry| {
let highest_time = highest_modification_times.get(&entry.file_name).unwrap();
entry.mtime == *highest_time
});
println!("- target_files");
let target_files = get_files(&config.target, &config.extensions)?;
println!("count source_files: {}", source_files.len());
println!("count target_files: {}", target_files.len());
source_files.retain(|file1| {
!target_files.iter().any(|file2| {
file1.file_name == file2.file_name
&& file1.size == file2.size
&& file1.mtime == file2.mtime
})
});
println!("count diff_files : {}", source_files.len());
for file in source_files {
let from = file.path;
let to = format!("{}/{}", config.target, file.file_name);
println!("diffcopy from: {} to: {}", from, to);
match copy_with_original_timestamp(from, to) {
Ok(_) => println!("File copied successfully with original timestamp."),
Err(e) => eprintln!("Error copying file: {}", e),
}
}
Ok(())
}
fn get_files(directory: &String, extensions: &[String]) -> Result<Vec<File>, Box<dyn Error>> {
let mut files: Vec<File> = vec![];
for entry in WalkDir::new(directory).into_iter().filter_map(|e| e.ok()).filter(|e| is_relevant(e, extensions)) {
let metadata = fs::metadata(entry.path())?;
let file_name = entry.file_name().to_string_lossy().to_string();
let size = metadata.len();
let ctime = 0; let mtime = metadata.modified()?.duration_since(UNIX_EPOCH)?.as_secs();
let path = entry.path().to_string_lossy().to_string();
let file = File {
file_name,
size,
ctime,
mtime,
path,
};
files.push(file);
}
Ok(files)
}
fn is_relevant(entry: &DirEntry, extensions: &[String]) -> bool {
let file_path = entry.path();
let file_extension = file_path.extension();
if !file_path.is_file() {
return false;
}
match file_extension {
Some(extension) => extensions.contains(&extension.to_string_lossy().to_lowercase()),
None => false,
}
}
fn copy_with_original_timestamp<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> io::Result<()> {
let metadata = fs::metadata(&from)?;
let mut permissions = metadata.permissions();
let mtime = FileTime::from_last_modification_time(&metadata);
fs::copy(&from, &to)?;
filetime::set_file_times(&to, mtime, mtime)?;
permissions.set_readonly(metadata.permissions().readonly());
fs::set_permissions(&to, permissions)?;
Ok(())
}