1use crate::scanner::{ScanListener, Scanner, Stats};
2use std::path::Path;
3use std::time::{Duration, Instant};
4
5#[derive(Debug)]
6struct Timing {
7 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 += ∁
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 += ∁
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}