wallswitch/
fileinfo.rs

1use crate::{
2    Config, Countable, Dimension, DimensionError, WallSwitchError, WallSwitchResult, exec_cmd,
3};
4use std::{
5    fmt,
6    fs::File,
7    hash::{DefaultHasher, Hasher},
8    io::{BufReader, Read},
9    path::PathBuf,
10    process::Command,
11    thread,
12};
13
14const BUFFER_SIZE: usize = 64 * 1024;
15
16/// Image information
17#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
18pub struct FileInfo {
19    /// File number (index + 1)
20    pub number: usize,
21    /// Total File number
22    pub total: usize,
23    /// dimension: width x length of an image
24    pub dimension: Dimension,
25    /// The size of the file, in bytes
26    pub size: u64,
27    /// AHash from Path
28    pub hash: String,
29    /// The image file path
30    pub path: PathBuf,
31}
32
33impl FileInfo {
34    /**
35    Returns true if the given pattern matches a sub-slice of this path.
36
37    Returns false if it does not.
38    */
39    pub fn path_contains(&self, string: &str) -> bool {
40        match self.path.to_str() {
41            Some(p) => p.contains(string),
42            None => false,
43        }
44    }
45
46    /// Update dimension field and valid_dimension field
47    pub fn update_info(&mut self, config: &Config) -> WallSwitchResult<()> {
48        // identify -format %wx%h image_file_path
49        let mut cmd = Command::new("identify");
50        let identify_cmd = cmd
51            .arg("-format")
52            .arg("%wx%h") // x separator
53            .arg(&self.path);
54
55        let identify_out = exec_cmd(identify_cmd, config.verbose, "identify")?;
56
57        let sdt_output = String::from_utf8(identify_out.stdout)?;
58
59        self.dimension = Dimension::new(&sdt_output)?;
60
61        Ok(())
62    }
63
64    /// Check if the dimension is valid.
65    pub fn dimension_is_valid(&self, config: &Config) -> bool {
66        let is_valid = self.dimension.is_valid(config);
67
68        if !is_valid {
69            // Instantiate the enum variant DimensionFormatError
70            let dim_error = DimensionError::DimensionFormatError {
71                dimension: self.dimension.clone(),
72                log_min: self.dimension.get_log_min(config),
73                log_max: self.dimension.get_log_max(config),
74                path: self.path.clone(),
75            };
76
77            eprintln!("{}", WallSwitchError::InvalidDimension(dim_error));
78        }
79
80        is_valid
81    }
82
83    /// Check if the size is valid.
84    pub fn size_is_valid(&self, config: &Config) -> bool {
85        self.size >= config.min_size && self.size <= config.max_size
86    }
87
88    pub fn name_is_valid(&self, config: &Config) -> bool {
89        let is_valid = self.path.file_name() != config.wallpaper.file_name();
90
91        if !is_valid && let Some(path) = self.path.file_name() {
92            eprintln!("{}\n", WallSwitchError::InvalidFilename(path.into()));
93        }
94
95        is_valid
96    }
97}
98
99/// FileInfo Extension
100pub trait FileInfoExt {
101    fn get_width_min(&self) -> Option<u64>;
102    fn get_max_size(&self) -> Option<u64>;
103    fn get_max_number(&self) -> Option<usize>;
104    fn get_max_dimension(&self) -> Option<u64>;
105    fn sizes_are_valid(&self, config: &Config) -> bool;
106    fn update_number(&mut self);
107    fn update_hash(&mut self) -> WallSwitchResult<()>;
108}
109
110impl FileInfoExt for [FileInfo] {
111    fn get_width_min(&self) -> Option<u64> {
112        self.iter().map(|file_info| file_info.dimension.width).min()
113    }
114
115    fn get_max_size(&self) -> Option<u64> {
116        self.iter().map(|file_info| file_info.size).max()
117    }
118
119    fn get_max_number(&self) -> Option<usize> {
120        self.iter().map(|file_info| file_info.number).max()
121    }
122
123    fn get_max_dimension(&self) -> Option<u64> {
124        self.iter()
125            .map(|file_info| file_info.dimension.maximum())
126            .max()
127    }
128
129    fn sizes_are_valid(&self, config: &Config) -> bool {
130        self.iter().all(|file_info| {
131            let is_valid = file_info.size_is_valid(config);
132
133            if !is_valid {
134                let size = file_info.size;
135
136                let min_size = config.min_size;
137                let max_size = config.max_size;
138
139                let path = file_info.path.clone();
140
141                // Print Indented file information
142                print!("{}", SliceDisplay(self));
143
144                eprintln!(
145                    "{}",
146                    WallSwitchError::InvalidSize {
147                        min_size,
148                        size,
149                        max_size,
150                    }
151                );
152                eprintln!("{}\n", WallSwitchError::DisregardPath(path));
153            }
154
155            is_valid
156        })
157    }
158
159    /// Update FileInfo number field
160    fn update_number(&mut self) {
161        let total = self.len();
162        self.iter_mut().enumerate().for_each(|(index, file)| {
163            file.number = index + 1;
164            file.total = total;
165        });
166    }
167
168    /// Update FileInfo hash field
169    fn update_hash(&mut self) -> WallSwitchResult<()> {
170        // Parallelize the computation using std::thread::scope
171        thread::scope(|scope| {
172            for file_info in self {
173                scope.spawn(move || -> WallSwitchResult<()> {
174                    // let id = thread::current().id();
175                    // println!("identifier thread: {id:?}");
176
177                    let file = File::open(&file_info.path)?;
178                    let reader = BufReader::with_capacity(BUFFER_SIZE, file);
179                    let hash = get_hash(reader)?;
180
181                    // println!("path: '{}' ; hash: '{}'", file_info.path.display(), hash);
182
183                    file_info.hash = hash;
184
185                    Ok(())
186                });
187            }
188        });
189
190        Ok(())
191    }
192}
193
194/// Calculates the hash from Path.
195///
196/// If the same stream of bytes is fed into each hasher, the same output will also be generated.
197///
198/// <https://doc.rust-lang.org/std/hash/trait.Hasher.html>
199pub fn get_hash(mut reader: impl Read) -> WallSwitchResult<String> {
200    let mut buffer = [0_u8; BUFFER_SIZE];
201    let mut hasher = DefaultHasher::new();
202
203    loop {
204        // read up to BUFFER_SIZE bytes to buffer
205        let count = reader.read(&mut buffer)?;
206        if count == 0 {
207            break;
208        }
209        hasher.write(&buffer[..count]);
210    }
211
212    Ok(hasher.finish().to_string())
213}
214
215/// Implement fmt::Display for Slice `[T]`
216///
217/// <https://stackoverflow.com/questions/30633177/implement-fmtdisplay-for-vect>
218///
219/// <https://stackoverflow.com/questions/33759072/why-doesnt-vect-implement-the-display-trait>
220///
221/// <https://gist.github.com/hyone/d6018ee1ac8f9496fed839f481eb59d6>
222pub struct SliceDisplay<'a>(pub &'a [FileInfo]);
223
224impl fmt::Display for SliceDisplay<'_> {
225    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
226        // The number of digits of maximum values
227        let digits_n: Option<usize> = self.0.get_max_number().map(|n| n.count_chars());
228        let digits_s: Option<usize> = self.0.get_max_size().map(|s| s.count_chars());
229        let digits_d: Option<usize> = self.0.get_max_dimension().map(|d| d.count_chars());
230
231        match (digits_n, digits_s, digits_d) {
232            (Some(num_digits_number), Some(num_digits_size), Some(num_digits_dimension)) => {
233                for file in self.0 {
234                    let dim = format!(
235                        "Dimension {{ width: {width:>d$}, height: {height:>d$} }}",
236                        width = file.dimension.width,
237                        height = file.dimension.height,
238                        d = num_digits_dimension,
239                    );
240
241                    writeln!(
242                        f,
243                        "images[{number:0n$}/{t}]: {dim}, size: {size:>s$}, path: {p:?}",
244                        number = file.number,
245                        n = num_digits_number,
246                        t = file.total,
247                        size = file.size,
248                        s = num_digits_size,
249                        p = file.path,
250                    )?;
251                }
252            }
253            _ => return Err(std::fmt::Error),
254        }
255
256        Ok(())
257    }
258}
259
260#[cfg(test)]
261mod test_info {
262    #[test]
263    /// `cargo test -- --show-output get_min_value_of_vec`
264    fn get_min_value_of_vec_v1() {
265        let values: Vec<i32> = vec![5, 6, 8, 4, 2, 7];
266
267        let min_value: Option<i32> = values.iter().min().copied();
268
269        println!("values: {values:?}");
270        println!("min_value: {min_value:?}");
271
272        assert_eq!(min_value, Some(2));
273    }
274
275    #[test]
276    /// `cargo test -- --show-output get_min_value_of_vec`
277    ///
278    /// <https://stackoverflow.com/questions/58669865/how-to-get-the-minimum-value-within-a-vector-in-rust>
279    fn get_min_value_of_vec_v2() {
280        let values: Vec<i32> = vec![5, 6, 8, 4, 2, 7];
281
282        // The empty vector must be filtered beforehand!
283        // let values: Vec<i32> = vec![]; // Not work!!!
284
285        // Get the minimum value without being wrapped by Option<T>
286        let min_value: i32 = values
287            .iter()
288            //.into_iter()
289            //.fold(i32::MAX, i32::min);
290            .fold(i32::MAX, |arg0: i32, other: &i32| i32::min(arg0, *other));
291
292        println!("values: {values:?}");
293        println!("min_value: {min_value}");
294
295        assert_eq!(min_value, 2);
296    }
297}