image_reducer/workspace/
mod.rs

1use std::{
2    collections::BTreeSet,
3    env::current_exe,
4    fs::{read, write},
5    path::{Path, PathBuf},
6    time::SystemTime,
7};
8
9use async_walkdir::{DirEntry, WalkDir};
10use bytesize::ByteSize;
11use futures::StreamExt;
12use log::LevelFilter;
13
14use colored::Colorize;
15use find_target::find_directory_or_create;
16
17use crate::{
18    utils::{hash_file, logger, optimize_png},
19    TinyResult,
20};
21
22mod config;
23
24pub struct TinyConfig {
25    pub writable: bool,
26    pub database: bool,
27    pub log_level: LevelFilter,
28}
29
30pub struct TinyWorkspace {
31    workspace: PathBuf,
32    writable: bool,
33    database: PathBuf,
34    reduced: u64,
35    start: SystemTime,
36    files: BTreeSet<u64>,
37}
38
39impl TinyWorkspace {
40    pub async fn check_all_pngs(&mut self) -> TinyResult {
41        let mut entries = WalkDir::new(&self.workspace);
42        loop {
43            let path = match entries.next().await {
44                Some(out) => match continue_search(out) {
45                    Some(path) => path,
46                    None => continue,
47                },
48                None => break,
49            };
50            if let Err(e) = self.optimize_png(&path) {
51                log::error!("{e}")
52            }
53        }
54        let reduced = ByteSize::b(self.reduced);
55        let timing = SystemTime::now().duration_since(self.start)?;
56        log::info!("Reduce {} in {:?}", reduced, timing);
57        Ok(())
58    }
59    pub fn optimize_png(&mut self, path: &Path) -> TinyResult {
60        let bytes = read(path)?;
61        let hash = hash_file(&bytes);
62        if self.files.contains(&hash) {
63            log::info!("Skip Optimized \n{}", path.display());
64            return Ok(());
65        }
66        match optimize_png(&bytes) {
67            Ok(o) => {
68                self.reduced += o.before.0 - o.after.0;
69                let reduce = format!("({:+.2}%)", o.reduce).green();
70                let file = self.relative_path(&path);
71                if self.writable {
72                    let overwrite = "(overwrite)".bold();
73                    log::info!("{} => {} {reduce}\n{} {overwrite}", o.before, o.after, file.display());
74                    write(path, &o.output)?;
75                    self.files.insert(hash_file(&o.output));
76                }
77                else {
78                    log::info!("{} => {} {reduce}\n{}", o.before, o.after, file.display());
79                }
80            }
81            Err(_) => {
82                if self.writable {
83                    self.files.insert(hash_file(&bytes));
84                }
85            }
86        };
87        Ok(())
88    }
89    fn relative_path(&self, target: &Path) -> PathBuf {
90        match pathdiff::diff_paths(target, &self.workspace) {
91            Some(s) => s,
92            None => target.to_path_buf(),
93        }
94    }
95}
96
97fn continue_search(r: Result<DirEntry, std::io::Error>) -> Option<PathBuf> {
98    let path = match r {
99        Ok(o) => o.path(),
100        Err(e) => {
101            log::error!("{e}");
102            return None;
103        }
104    };
105    if !path.is_file() {
106        return None;
107    }
108    let name = path.file_name()?.to_str()?;
109    if name.ends_with(".png") { Some(path) } else { None }
110}