change/
lib.rs

1#![feature(let_chains)]
2
3use std::{
4  collections::HashMap,
5  fs::File,
6  hash::Hasher,
7  io::{BufRead, BufReader, Read},
8  path::{Path, PathBuf},
9};
10
11use serde::{Deserialize, Serialize};
12use aok::{Void, OK};
13use set_mtime::set_mtime;
14pub use walkdir::WalkDir;
15use xxhash_rust::xxh3::Xxh3DefaultBuilder;
16
17pub fn hash(fp: impl AsRef<Path>) -> std::io::Result<u128> {
18  let file = File::open(fp)?;
19  let mut reader = BufReader::new(file);
20  let mut hasher = Xxh3DefaultBuilder.build();
21
22  let mut buffer = [0; 16384];
23  loop {
24    let bytes_read = reader.read(&mut buffer)?;
25    if bytes_read == 0 {
26      break;
27    }
28    hasher.write(&buffer[..bytes_read]);
29  }
30
31  Ok(hasher.digest128())
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct LenTs {
36  pub ts: u64,
37  pub len: u64,
38}
39
40#[derive(Debug, Serialize, Deserialize)]
41pub struct Meta {
42  pub len_ts: LenTs,
43  pub hash: u128,
44}
45
46#[derive(Debug)]
47pub struct Scan {
48  pub root: PathBuf,
49  pub rel_len_ts: HashMap<String, LenTs>,
50}
51
52#[derive(Debug)]
53pub struct Diff {
54  pub changed: Vec<(String, Meta)>,
55  pub no_change: Vec<(String, Meta)>,
56  pub db: PathBuf,
57  pub has_change: bool,
58}
59
60impl Diff {
61  pub fn save(&self) -> Void {
62    let mut li = vec![];
63    for (rel, meta) in self.changed.iter().chain(&self.no_change) {
64      let meta = burl::e(pc::e::<Meta>(meta)?);
65      li.push(format!("{rel}#{meta}"));
66    }
67
68    let db = li.join("\n");
69    li.sort();
70
71    ifs::wstr(&self.db, db)?;
72
73    OK
74  }
75}
76
77impl Scan {
78  pub fn diff(&self, db: impl Into<PathBuf>) -> std::io::Result<Diff> {
79    let mut changed = vec![];
80    let mut no_change = vec![];
81    let db = db.into();
82    let mut rel_len_ts = self.rel_len_ts.clone();
83    if db.exists() {
84      let file = std::io::BufReader::new(std::fs::File::open(&db)?);
85      for line in file.lines().map_while(Result::ok) {
86        let line = line.trim_end();
87        if let Some(i) = line.chars().next() {
88          if "<>#".contains(i) {
89            continue;
90          }
91
92          if let Some(pos) = line.rfind('#') {
93            let bin = &line[pos + 1..];
94            if let Ok(meta) = burl::d(bin) {
95              if let Ok::<Meta, _>(meta) = pc::d(&meta) {
96                let rel = &line[..pos];
97                if let Some(len_ts) = rel_len_ts.remove(rel) {
98                  if len_ts.len == meta.len_ts.len && len_ts.ts == meta.len_ts.ts {
99                    no_change.push((rel.into(), meta));
100                  } else {
101                    let fp = self.root.join(rel);
102                    let hash = hash(&fp)?;
103                    if hash == meta.hash {
104                      set_mtime(&fp, meta.len_ts.ts)?;
105                      no_change.push((
106                        rel.into(),
107                        Meta {
108                          len_ts: LenTs {
109                            len: len_ts.len,
110                            ts: meta.len_ts.ts,
111                          },
112                          hash,
113                        },
114                      ));
115                    } else {
116                      changed.push((rel.into(), Meta { len_ts, hash }));
117                    }
118                  }
119                }
120              }
121            }
122          }
123        } else {
124          continue;
125        }
126      }
127    }
128
129    for (rel, len_ts) in rel_len_ts.drain() {
130      let fp = self.root.join(&rel);
131      changed.push((
132        rel,
133        Meta {
134          len_ts,
135          hash: hash(fp)?,
136        },
137      ));
138    }
139
140    Ok(Diff {
141      has_change: !changed.is_empty() || no_change.len() != self.rel_len_ts.len(),
142      changed,
143      db,
144      no_change,
145    })
146  }
147
148  pub fn add(&mut self, rel: impl AsRef<str>) -> Void {
149    let rel = rel.as_ref();
150    let fp = self.root.join(rel);
151    if let Ok(meta) = std::fs::metadata(fp)
152      && let Ok(ts) = meta.modified()
153    {
154      self.rel_len_ts.insert(
155        rel.into(),
156        LenTs {
157          ts: ts.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(),
158          len: meta.len(),
159        },
160      );
161    }
162    OK
163  }
164
165  pub fn new(root: impl Into<PathBuf>) -> std::io::Result<Self> {
166    let root = root.into();
167    let mut rel_len_ts = HashMap::default();
168
169    for entry in WalkDir::new(&root).into_iter() {
170      if let Ok(entry) = entry
171        && entry.file_type().is_file()
172      {
173        let path = entry.path();
174        if let Ok(meta) = std::fs::metadata(path)
175          && let Ok(ts) = meta.modified()
176          && let Ok(rel) = path.strip_prefix(&root)
177        {
178          let rel = rel.to_string_lossy();
179          let rel = ifs::unix_path(rel);
180          rel_len_ts.insert(
181            rel,
182            LenTs {
183              ts: ts.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(),
184              len: meta.len(),
185            },
186          );
187        }
188      }
189    }
190
191    Ok(Self { rel_len_ts, root })
192  }
193}