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 {
    // Time in seconds, used to throttle console output
    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 += &comp;
        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 += &comp;
    }
    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");
}