jk/
commands.rs

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}