diffcopy/
lib.rs

1use filetime::FileTime;
2use std::error::Error;
3use std::fs;
4use std::io;
5use std::path::Path;
6use std::time::UNIX_EPOCH;
7use std::collections::HashMap;
8use clap::{Arg, ArgMatches, Command, crate_version};
9use walkdir::{DirEntry, WalkDir};
10
11pub struct Config {
12    pub source: String,
13    pub target: String,
14    pub extensions: Vec<String>,
15}
16
17impl Config {
18    pub fn new(matches: ArgMatches) -> Config {
19        let source = matches.get_one::<String>("source-dir").unwrap().to_string();
20        let target = matches.get_one::<String>("target-dir").unwrap().to_string();
21        let extensions: Vec<String> = matches.get_one::<String>("file-extensions").unwrap().to_string().split(',').map(|s| s.to_string()).collect(); 
22	
23        Config { source, target, extensions }
24    }
25}
26
27
28#[derive(Debug, PartialEq, Eq)]
29struct File {
30    file_name: String,
31    size: u64,
32    ctime: u64,
33    mtime: u64,
34    path: String,
35}
36
37pub fn build_clap() -> ArgMatches {
38    Command::new("diffcopy")
39        .author("Rolf Speer <rolf.speer@gmail.com>")
40        .about("Copy modified files from sub directories into one target directory")
41        .arg(
42            Arg::new("source-dir")
43                .value_name("source-dir")
44                .help("source directory for searching for the changed files (incl. subdirectories)")
45                .index(1)
46                .required(true),
47        )
48        .arg(
49            Arg::new("target-dir")
50                .value_name("target-dir")
51                .help("target directory for storing the copied files")
52                .index(2)
53                .required(true),
54        )
55        .arg(
56            Arg::new("file-extensions")
57                .value_name("file-extensions")
58                .help("all file extensions to be searched for")
59                .index(3)
60                .required(true)
61        ) 
62        .version(crate_version!())
63        .get_matches()
64}
65
66pub fn run(config: &Config) -> Result<(), Box<dyn Error>> {
67    
68    println!("source directory: {}", config.source);
69    println!("target directory: {}", config.target);
70    
71    let extensions_string = config.extensions.join("|");
72    println!("extensions      : {}", extensions_string);
73
74    println!("- source_files");
75    let mut source_files = get_files(&config.source, &config.extensions)?;
76
77    let mut highest_modification_times: HashMap<String, u64> = HashMap::new();
78
79    for entry in &source_files {
80        let filename = entry.file_name.clone();
81        let mtime = entry.mtime;
82        if let Some(current_time) = highest_modification_times.get_mut(&filename) {
83            if mtime > *current_time {
84                *current_time = mtime;
85            }
86        } else {
87            highest_modification_times.insert(filename.clone(), mtime);
88        }
89    }
90
91    source_files.retain(|entry| {
92        let highest_time = highest_modification_times.get(&entry.file_name).unwrap();
93        entry.mtime == *highest_time
94    });
95
96    println!("- target_files");
97    let target_files = get_files(&config.target, &config.extensions)?;
98
99    println!("count source_files: {}", source_files.len());
100    println!("count target_files: {}", target_files.len());
101
102    source_files.retain(|file1| {
103        !target_files.iter().any(|file2| {
104            file1.file_name == file2.file_name
105                && file1.size == file2.size
106                && file1.mtime == file2.mtime
107        })
108    });
109
110    println!("count diff_files  : {}", source_files.len());
111
112    for file in source_files {
113        let from = file.path;
114        let to = format!("{}/{}", config.target, file.file_name);
115        println!("diffcopy from: {} to: {}", from, to);
116        match copy_with_original_timestamp(from, to) {
117            Ok(_) => println!("File copied successfully with original timestamp."),
118            Err(e) => eprintln!("Error copying file: {}", e),
119        }
120    }
121
122    Ok(())
123}
124
125fn get_files(directory: &String, extensions: &[String]) -> Result<Vec<File>, Box<dyn Error>> {
126    let mut files: Vec<File> = vec![];
127
128    for entry in WalkDir::new(directory).into_iter().filter_map(|e| e.ok()).filter(|e| is_relevant(e, extensions)) {
129
130        let metadata = fs::metadata(entry.path())?;
131
132        let file_name = entry.file_name().to_string_lossy().to_string();
133        let size = metadata.len();
134
135
136	let ctime = 0; // metadata.created()?.duration_since(UNIX_EPOCH)?.as_secs(); 
137        let mtime = metadata.modified()?.duration_since(UNIX_EPOCH)?.as_secs();
138        let path = entry.path().to_string_lossy().to_string();
139
140        // println!("  entry: {}, mtime: {}, size: {}", entry.file_name().to_string_lossy(), mtime, size);
141
142        let file = File {
143            file_name,
144            size,
145            ctime,
146            mtime,
147            path,
148        };
149        files.push(file);
150    }
151
152    Ok(files)
153}
154
155fn is_relevant(entry: &DirEntry, extensions: &[String]) -> bool {
156
157    let file_path = entry.path();
158    let file_extension = file_path.extension();
159
160    if !file_path.is_file() {
161        return false;
162    }
163
164    match file_extension {
165        Some(extension) => extensions.contains(&extension.to_string_lossy().to_lowercase()),
166        None => false,
167    }
168}
169
170fn copy_with_original_timestamp<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> io::Result<()> {
171
172    let metadata = fs::metadata(&from)?;
173    let mut permissions = metadata.permissions();
174    let mtime = FileTime::from_last_modification_time(&metadata);
175
176    fs::copy(&from, &to)?;
177
178    filetime::set_file_times(&to, mtime, mtime)?;
179    permissions.set_readonly(metadata.permissions().readonly());
180    fs::set_permissions(&to, permissions)?;
181
182    Ok(())
183}
184