1use ansi_term::Colour;
2use chrono::TimeZone;
3use chrono::Utc;
4use regex::Regex;
5use std::time::SystemTime;
6use std::{
7 collections::HashMap,
8 ffi::OsStr,
9 fs as fs_sync,
10 io::stdin,
11 path::{Path, PathBuf},
12};
13use tokio::fs;
14use tracing::{debug, error, info, warn};
15use walkdir::WalkDir;
16
17use crate::config::{Config, FileType};
18
19pub fn list(config: &Config) {
20 return WalkDir::new(&config.src)
21 .into_iter()
22 .filter_map(|file| file.ok())
23 .filter(|file| match config.file_type {
24 FileType::File => file.file_type().is_file(),
25 FileType::Dir => file.file_type().is_dir(),
26 FileType::Both => true,
27 })
28 .filter(|file| {
29 if let Some(name) = &config.name {
30 return file.path().to_string_lossy().contains(name);
31 } else {
32 true
33 }
34 })
35 .for_each(|file| {
36 info!(
37 "{} (created_at {})",
38 file.path().display(),
39 Utc.timestamp(
40 file.metadata()
41 .unwrap()
42 .created()
43 .unwrap()
44 .duration_since(SystemTime::UNIX_EPOCH)
45 .unwrap()
46 .as_secs() as i64,
47 0
48 )
49 )
50 });
51}
52
53pub fn list_duplicates(config: &Config) {
54 let mut files = HashMap::new();
55
56 WalkDir::new(&config.src)
57 .into_iter()
58 .filter_map(|file| file.ok())
59 .filter(|file| match config.file_type {
60 FileType::File => file.file_type().is_file(),
61 FileType::Dir => file.file_type().is_dir(),
62 FileType::Both => true,
63 })
64 .filter(|file| {
65 if let Some(name) = &config.name {
66 return file.path().to_string_lossy().contains(name);
67 } else {
68 true
69 }
70 })
71 .for_each(|file| {
72 let f_name = String::from(file.file_name().to_string_lossy());
73 *files.entry(f_name).or_insert(0) += 1;
74 });
75
76 for (key, value) in &files {
77 if *value > 1 {
78 info!("{}: {}", key, Colour::Yellow.paint(value.to_string()));
79 }
80 }
81}
82
83pub fn add_date(config: &Config) {
84 WalkDir::new(&config.src)
85 .into_iter()
86 .filter_map(|file| file.ok())
87 .filter(|file| file.file_type().is_file())
88 .filter(|file| {
89 if let Some(name) = &config.name {
90 return file.path().to_string_lossy().contains(name);
91 } else {
92 true
93 }
94 })
95 .filter(|file| {
96 let ext = file
97 .path()
98 .extension()
99 .and_then(OsStr::to_str)
100 .unwrap_or_else(|| "");
101
102 if ext.contains("jpg") || ext.contains("jpeg") || ext.contains("jpeg") {
103 return true;
104 }
105
106 false
107 })
108 .for_each(|file| {
109 let re = Regex::new(r"^\d{8}_").unwrap();
110 let file_name = file.file_name().to_str().unwrap();
111 if re.is_match(file_name) {
112 debug!("{} already has a date", file_name);
113 return;
114 }
115
116 println!(
117 "add date to {} in {}?",
118 file_name,
119 file.path()
120 .parent()
121 .expect("couldn't get parent path")
122 .display()
123 );
124 let mut answer = String::new();
125 stdin().read_line(&mut answer).expect("failed to readline");
126 answer = answer.trim().to_lowercase();
127
128 if answer != "yes" && answer != "y" {
129 return;
130 }
131
132 let file_buffer = fs_sync::read(file.path()).unwrap();
133
134 match rexif::parse_buffer_quiet(&file_buffer) {
135 (Ok(exif), _warnings) => {
136 for entry in &exif.entries {
137 if entry.tag == rexif::ExifTag::DateTime {
138 let date = format!(
139 "{}{}{}",
140 &entry.value_more_readable[..4],
141 &entry.value_more_readable[5..7],
142 &entry.value_more_readable[8..10]
143 );
144 let new_file_name = &format!("{}_{}", date, file_name);
145 let new_file_path = Path::new(file.path().parent().unwrap())
146 .join(PathBuf::from(new_file_name));
147
148 debug!(
149 "file will be renamed to {}",
150 &new_file_path.to_path_buf().display()
151 );
152
153 match fs_sync::rename(file.path(), new_file_path.clone()) {
154 Ok(_) => {
155 info!(
156 "renamed a file {} -> {}",
157 file.path().display(),
158 new_file_path.display()
159 )
160 }
161 Err(e) => {
162 error!(
163 "failed to rename a file {} {}",
164 file.path().display(),
165 e
166 )
167 }
168 }
169 }
170 }
171 }
172 (Err(e), _warnings) => {
173 warn!("Error reading exif {}: {}", &file_name, e);
174 }
175 }
176 });
177}
178
179pub fn move_to_month_dir(config: &Config) {
180 let source = config.src.clone();
181 WalkDir::new(&config.src)
182 .into_iter()
183 .filter_map(|file| file.ok())
184 .filter(|file| file.file_type().is_file())
185 .filter(|file| {
186 if let Some(name) = &config.name {
187 return file.path().to_string_lossy().contains(name);
188 } else {
189 true
190 }
191 })
192 .filter(|file| {
193 let ext = file
194 .path()
195 .extension()
196 .and_then(OsStr::to_str)
197 .unwrap_or_else(|| "");
198
199 if ext.contains("jpg") || ext.contains("jpeg") || ext.contains("jpeg") {
200 return true;
201 }
202
203 false
204 })
205 .for_each(|file| {
206 let dir_re = Regex::new(r"/\d{4}/\d{2}$").unwrap();
207 let file_name_re = Regex::new(r"^\d{8}_").unwrap();
208
209 let dir = file.path().parent().unwrap().to_str().unwrap();
210 let file_name = file.file_name().to_str().unwrap();
211
212 if dir_re.is_match(dir) {
213 debug!("{} already in correct dir", file.path().display());
214 return;
215 } else {
216 if !file_name_re.is_match(file_name) {
217 warn!("{} file missing date", dir);
218 return;
219 }
220 }
221
222 let year = &file_name[..4];
223 let month = &file_name[4..6];
224 let new_path = Path::new(&source).join(year).join(month).join(file_name);
225
226 println!(
227 "move file {} to {}?",
228 file.path().display(),
229 new_path.display()
230 );
231 let mut answer = String::new();
232 stdin().read_line(&mut answer).expect("failed to readline");
233 answer = answer.trim().to_lowercase();
234
235 if answer != "yes" && answer != "y" {
236 return;
237 }
238
239 let new_path_dir = new_path.parent().unwrap();
240
241 let res = fs_sync::create_dir_all(&new_path_dir);
242
243 if res.is_ok() {
244 debug!("created a dir {}", &new_path_dir.display())
245 } else {
246 error!("failed to create dir {}", &new_path_dir.display());
247 return;
248 }
249
250 match fs_sync::rename(file.path(), new_path.clone()) {
251 Ok(_) => {
252 info!(
253 "moved a file {} -> {}",
254 file.path().display(),
255 new_path.display()
256 )
257 }
258 Err(e) => {
259 error!("failed to move a file {} {}", file.path().display(), e)
260 }
261 }
262 });
263}
264
265pub async fn remove_dup(config: &Config) {
266 let mut files = HashMap::new();
267 struct FileEntry {
268 count: u32,
269 paths: Vec<PathBuf>,
270 }
271
272 WalkDir::new(&config.src)
273 .into_iter()
274 .filter_map(|file| file.ok())
275 .filter(|file| file.file_type().is_file())
276 .filter(|file| {
277 if let Some(name) = &config.name {
278 return file.path().to_string_lossy().contains(name);
279 } else {
280 true
281 }
282 })
283 .for_each(|file| {
284 let f_name = String::from(file.file_name().to_string_lossy());
285 files
286 .entry(f_name)
287 .and_modify(|e: &mut FileEntry| {
288 e.count += 1;
289 e.paths.push(file.path().to_path_buf());
290 })
291 .or_insert(FileEntry {
292 count: 1,
293 paths: vec![file.path().to_path_buf()],
294 });
295 });
296
297 for (key, file_entry) in &files {
298 let mut deleted_count = 0;
299 if file_entry.count > 1 {
300 info!(
301 "{}: {}\n{:?}",
302 key,
303 Colour::Yellow.paint(file_entry.count.to_string()),
304 file_entry.paths
305 );
306 for file_path in file_entry.paths.iter() {
307 println!("{} {}", Colour::Red.paint("delete?"), file_path.display());
308 let mut answer = String::new();
309 stdin().read_line(&mut answer).expect("failed to readline");
310 answer = answer.trim().to_lowercase();
311
312 if answer == "yes" || answer == "y" {
313 match fs::remove_file(file_path).await {
314 Ok(_) => {
315 debug!("deleted a file {}", file_path.display())
316 }
317 Err(e) => {
318 error!("couldn't delete a file {} {}", file_path.display(), e)
319 }
320 }
321
322 deleted_count += 1;
323 }
324 }
325
326 info!(
327 "done deleting {} file(s)\n",
328 Colour::Green.paint(deleted_count.to_string())
329 );
330 }
331 }
332}
333
334pub fn remove(config: &Config) {
335 let name = config.name.as_ref().expect("missing file name");
336
337 let mut deleted_count = 0;
338 WalkDir::new(&config.src)
339 .into_iter()
340 .filter_map(|file| file.ok())
341 .filter(|file| match config.file_type {
342 FileType::File => file.file_type().is_file(),
343 FileType::Dir => file.file_type().is_dir(),
344 FileType::Both => file.file_type().is_dir(),
345 })
346 .filter(|file| file.path().to_string_lossy().contains(name))
347 .for_each(|file| {
348 println!("{} {}", Colour::Red.paint("delete?"), file.path().display());
349 let mut answer = String::new();
350 stdin().read_line(&mut answer).expect("failed to readline");
351 answer = answer.trim().to_lowercase();
352
353 if answer != "yes" && answer != "y" {
354 return;
355 }
356
357 match config.file_type {
358 FileType::File => fs_sync::remove_file(file.path()).unwrap_or_else(|_err| {
359 error!("couldn't delete a file {}", file.path().display())
360 }),
361 FileType::Dir => fs_sync::remove_dir_all(file.path()).unwrap_or_else(|_err| {
362 error!("couldn't delete a dir {}", file.path().display())
363 }),
364 FileType::Both => {
365 if file.file_type().is_file() || file.file_type().is_symlink() {
366 fs_sync::remove_file(file.path()).unwrap_or_else(|_err| {
367 error!("couldn't delete a file {}", file.path().display())
368 });
369 }
370 if file.file_type().is_dir() {
371 fs_sync::remove_dir_all(file.path()).unwrap_or_else(|_err| {
372 error!("couldn't delete a dir {}", file.path().display())
373 });
374 }
375 }
376 }
377 deleted_count += 1;
378 });
379
380 info!(
381 "done deleting {} file(s)",
382 Colour::Green.paint(deleted_count.to_string())
383 );
384}