dupe_krill/
ui.rs

1use crate::scanner::{ScanListener, Scanner, Stats};
2use std::path::Path;
3use std::time::{Duration, Instant};
4
5#[derive(Debug)]
6struct Timing {
7    // Time in seconds, used to throttle console output
8    next_update: u64,
9    start_time: Instant,
10}
11
12#[derive(Debug)]
13pub struct UI {
14    timing: Timing,
15}
16
17impl UI {
18    #[must_use]
19    pub fn new() -> Self {
20        Self {
21            timing: Timing {
22                next_update: 0,
23                start_time: Instant::now(),
24            },
25        }
26    }
27}
28
29impl ScanListener for UI {
30    fn file_scanned(&mut self, path: &Path, stats: &Stats) {
31        let elapsed = self.timing.start_time.elapsed().as_secs();
32        if elapsed > self.timing.next_update {
33            self.timing.next_update = elapsed+1;
34            println!("{}+{}+{} dupes ({} saved). {}+{} files scanned. {}/…",
35                stats.dupes, stats.hardlinks, stats.reflinks, human_size(stats.bytes_deduplicated), stats.added, stats.skipped,
36                path.parent().unwrap_or(path).display());
37        }
38    }
39
40    #[allow(overlapping_range_endpoints)]
41    fn scan_over(&self, _: &Scanner, stats: &Stats, scan_duration: Duration) {
42        let nice_duration = match scan_duration.as_secs() {
43            x @ 0..=5 => format!("{:.1}s", (x * 1_000_000_000 + u64::from(scan_duration.subsec_nanos())) as f64 / 1_000_000_000f64),
44            x @ 5..=59 => format!("{x}s"),
45            x => format!("{}m{}s", x / 60, x % 60),
46        };
47        println!("Dupes found: {}, wasting {}. Existing hardlinks: {}, saving {}. Reflinks created: {}, saving {}. Scanned: {}. Skipped {}. Total scan duration: {}",
48            stats.dupes, human_size(stats.bytes_deduplicated), stats.hardlinks, human_size(stats.bytes_saved_by_hardlinks),
49            stats.reflinks, human_size(stats.bytes_saved_by_reflinks), stats.added, stats.skipped, nice_duration);
50    }
51
52    fn hardlinked(&mut self, src: &Path, dst: &Path) {
53        println!("Hardlinked {}", combined_paths(src, dst));
54    }
55
56    fn reflinked(&mut self, src: &Path, dst: &Path) {
57        println!("Reflinked {}", combined_paths(src, dst));
58    }
59
60    fn duplicate_found(&mut self, src: &Path, dst: &Path) {
61        println!("Found dupe {}", combined_paths(src, dst));
62    }
63}
64
65const POWERS_OF_TWO: [&str; 7] = ["", "k", "M", "G", "T", "P", "E"];
66fn human_size(size: usize) -> String {
67    let power_threshold = 1024.;
68
69    let mut current_power = 0;
70    let mut current_power_size = size as f64;
71
72    while current_power_size >= power_threshold {
73        current_power_size /= 1000_f64;
74        current_power += 1;
75    }
76
77    format!("{:.2}{}B", current_power_size, POWERS_OF_TWO[current_power])
78}
79
80fn combined_paths(base: &Path, relativize: &Path) -> String {
81    let base: Vec<_> = base.iter().collect();
82    let relativize: Vec<_> = relativize.iter().collect();
83
84    let mut out = String::with_capacity(80);
85    let mut prefix_len = 0;
86    for (comp, _) in base.iter().zip(relativize.iter()).take_while(|&(a, b)| a == b) {
87        prefix_len += 1;
88        let comp = comp.to_string_lossy();
89        out += &comp;
90        if comp != "/" {
91            out.push('/');
92        }
93    }
94
95    let suffix: Vec<_> = base.iter().skip(prefix_len).rev().zip(relativize.iter().skip(prefix_len).rev())
96        .take_while(|&(a,b)| a==b).map(|(_,b)|b.to_string_lossy()).collect();
97
98    let base_unique: Vec<_> = base[prefix_len..base.len() - suffix.len()].iter().map(|b| b.to_string_lossy()).collect();
99
100    out.push('{');
101    if base_unique.is_empty() {
102        out.push('.');
103    } else {
104        out += &base_unique.join("/");
105    }
106    out += " => ";
107
108    let rel_unique: Vec<_> = relativize[prefix_len..relativize.len() - suffix.len()]
109        .iter()
110        .map(|b| b.to_string_lossy())
111        .collect();
112    if rel_unique.is_empty() {
113        out.push('.');
114    } else {
115        out += &rel_unique.join("/");
116    }
117    out.push('}');
118
119    for comp in suffix.into_iter().rev() {
120        out.push('/');
121        out += &comp;
122    }
123    out
124}
125
126#[test]
127fn combined_test() {
128    use std::path::PathBuf;
129    let a: PathBuf = "foo/bar/baz/a.txt".into();
130    let b: PathBuf = "foo/baz/quz/zzz/a.txt".into();
131    let c: PathBuf = "foo/baz/quz/zzz/b.txt".into();
132    let d: PathBuf = "b.txt".into();
133    let e: PathBuf = "e.txt".into();
134    let f: PathBuf = "/foo/bar/baz/a.txt".into();
135    let g: PathBuf = "/foo/baz/quz/zzz/a.txt".into();
136    let h: PathBuf = "/foo/b/quz/zzz/a.txt".into();
137
138    assert_eq!(&combined_paths(&a, &b), "foo/{bar/baz => baz/quz/zzz}/a.txt");
139    assert_eq!(&combined_paths(&c, &b), "foo/baz/quz/zzz/{b.txt => a.txt}");
140    assert_eq!(&combined_paths(&c, &d), "{foo/baz/quz/zzz => .}/b.txt");
141    assert_eq!(&combined_paths(&d, &c), "{. => foo/baz/quz/zzz}/b.txt");
142    assert_eq!(&combined_paths(&d, &e), "{b.txt => e.txt}");
143    assert_eq!(&combined_paths(&f, &g), "/foo/{bar/baz => baz/quz/zzz}/a.txt");
144    assert_eq!(&combined_paths(&h, &g), "/foo/{b => baz}/quz/zzz/a.txt");
145}
146
147#[test]
148fn human_size_test() {
149    assert_eq!(human_size(15632), "15.63kB");
150    assert_eq!(human_size(1563244), "1.56MB");
151    assert_eq!(human_size(1563244174), "1.56GB");
152    assert_eq!(human_size(1563244928194), "1.56TB");
153}