diffcopy 0.1.1

Copy modified files from sub directories into one target directory.
Documentation
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; // metadata.created()?.duration_since(UNIX_EPOCH)?.as_secs(); 
        let mtime = metadata.modified()?.duration_since(UNIX_EPOCH)?.as_secs();
        let path = entry.path().to_string_lossy().to_string();

        // println!("  entry: {}, mtime: {}, size: {}", entry.file_name().to_string_lossy(), mtime, size);

        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(())
}