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; let mtime = metadata.modified()?.duration_since(UNIX_EPOCH)?.as_secs();
138 let path = entry.path().to_string_lossy().to_string();
139
140 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