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}