1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
use std::time::{Instant,Duration};
use scanner::Stats;
use scanner::ScanListener;
use scanner::Scanner;
use std::path::PathBuf;
use std::path::Path;
#[derive(Debug)]
struct Timing {
next_update: u64,
start_time: Instant,
}
#[derive(Debug)]
pub struct UI {
timing: Timing,
}
impl UI {
pub fn new() -> Self {
UI {
timing: Timing {
next_update: 0,
start_time: Instant::now(),
},
}
}
}
impl ScanListener for UI {
fn file_scanned(&mut self, path: &PathBuf, stats: &Stats) {
let elapsed = self.timing.start_time.elapsed().as_secs();
if elapsed > self.timing.next_update {
self.timing.next_update = elapsed+1;
println!("{}+{} dupes. {}+{} files scanned. {}/…",
stats.dupes, stats.hardlinks, stats.added, stats.skipped,
path.parent().unwrap_or(path).display());
}
}
fn scan_over(&self, _: &Scanner, stats: &Stats, scan_duration: Duration) {
let nice_duration = match scan_duration.as_secs() {
x @ 0 ... 5 => format!("{:.1}s", (x * 1_000_000_000 + scan_duration.subsec_nanos() as u64) as f64 / 1_000_000_000f64),
x @ 5 ... 59 => format!("{}s", x),
x => format!("{}m{}s", x/60, x%60),
};
println!("Dupes found: {}. Existing hardlinks: {}. Scanned: {}. Skipped {}. Total scan duration: {}",
stats.dupes, stats.hardlinks, stats.added, stats.skipped, nice_duration);
}
fn hardlinked(&mut self, src: &Path, dst: &Path) {
println!("Hardlinked {}", combined_paths(src, dst));
}
fn duplicate_found(&mut self, src: &Path, dst: &Path) {
println!("Found dupe {}", combined_paths(src, dst));
}
}
fn combined_paths(base: &Path, relativize: &Path) -> String {
let base: Vec<_> = base.iter().collect();
let relativize: Vec<_> = relativize.iter().collect();
let mut out = String::with_capacity(80);
let mut prefix_len = 0;
for (comp,_) in base.iter().zip(relativize.iter()).take_while(|&(a,b)| a==b) {
prefix_len += 1;
let comp = comp.to_string_lossy();
out += ∁
if comp != "/" {
out.push('/');
}
}
let suffix: Vec<_> = base.iter().skip(prefix_len).rev().zip(relativize.iter().skip(prefix_len).rev())
.take_while(|&(a,b)| a==b).map(|(_,b)|b.to_string_lossy()).collect();
let base_unique: Vec<_> = base[prefix_len..base.len()-suffix.len()].iter().map(|b|b.to_string_lossy()).collect();
out.push('{');
if base_unique.is_empty() {
out.push('.');
} else {
out += &base_unique.join("/");
}
out += " => ";
let rel_unique: Vec<_> = relativize[prefix_len..relativize.len()-suffix.len()].iter().map(|b|b.to_string_lossy()).collect();
if rel_unique.is_empty() {
out.push('.');
} else {
out += &rel_unique.join("/");
}
out.push('}');
for comp in suffix.into_iter().rev() {
out.push('/');
out += ∁
}
out
}
#[test]
fn combined_test() {
let a: PathBuf = "foo/bar/baz/a.txt".into();
let b: PathBuf = "foo/baz/quz/zzz/a.txt".into();
let c: PathBuf = "foo/baz/quz/zzz/b.txt".into();
let d: PathBuf = "b.txt".into();
let e: PathBuf = "e.txt".into();
let f: PathBuf = "/foo/bar/baz/a.txt".into();
let g: PathBuf = "/foo/baz/quz/zzz/a.txt".into();
let h: PathBuf = "/foo/b/quz/zzz/a.txt".into();
assert_eq!(&combined_paths(&a,&b), "foo/{bar/baz => baz/quz/zzz}/a.txt");
assert_eq!(&combined_paths(&c,&b), "foo/baz/quz/zzz/{b.txt => a.txt}");
assert_eq!(&combined_paths(&c,&d), "{foo/baz/quz/zzz => .}/b.txt");
assert_eq!(&combined_paths(&d,&c), "{. => foo/baz/quz/zzz}/b.txt");
assert_eq!(&combined_paths(&d,&e), "{b.txt => e.txt}");
assert_eq!(&combined_paths(&f,&g), "/foo/{bar/baz => baz/quz/zzz}/a.txt");
assert_eq!(&combined_paths(&h,&g), "/foo/{b => baz}/quz/zzz/a.txt");
}