1use std::path::{Path, PathBuf};
9use std::time::Duration;
10
11const EPOCH_MARKER: &str = "pushkin-epoch:";
14
15const PROBE_HEAD_BYTES: usize = 512;
18
19#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum RegenOutcome {
23 Regenerated,
24 TimedOut,
25 Failed(String),
26}
27
28pub fn probe_stale(dir: &Path, expected_epoch: u32) -> std::io::Result<Vec<PathBuf>> {
35 let mut stale = Vec::new();
36 let mut pending = vec![dir.to_path_buf()];
37 while let Some(current) = pending.pop() {
38 for entry in std::fs::read_dir(¤t)? {
39 let entry = entry?;
40 let path = entry.path();
41 if path.is_dir() {
42 pending.push(path);
43 } else if file_epoch(&path)? != Some(expected_epoch) {
44 if let Ok(relative) = path.strip_prefix(dir) {
45 stale.push(relative.to_path_buf());
46 }
47 }
48 }
49 }
50 Ok(stale)
51}
52
53fn file_epoch(path: &Path) -> std::io::Result<Option<u32>> {
57 use std::io::Read;
58 let mut head = vec![0_u8; PROBE_HEAD_BYTES];
59 let mut file = std::fs::File::open(path)?;
60 let read = file.read(&mut head)?;
61 head.truncate(read);
62 let text = String::from_utf8_lossy(&head);
63 Ok(text.lines().find_map(|line| {
64 let (_, rest) = line.split_once(EPOCH_MARKER)?;
65 rest.trim().parse::<u32>().ok()
66 }))
67}
68
69pub fn run_queue<W>(items: &[PathBuf], timeout: Duration, worker: W) -> Vec<(PathBuf, RegenOutcome)>
75where
76 W: Fn(&Path) -> Result<(), String> + Send + Sync + 'static,
77{
78 let worker = std::sync::Arc::new(worker);
79 let mut outcomes = Vec::with_capacity(items.len());
80 for item in items {
81 outcomes.push((item.clone(), run_one(item, timeout, &worker)));
82 }
83 outcomes
84}
85
86fn run_one<W>(item: &Path, timeout: Duration, worker: &std::sync::Arc<W>) -> RegenOutcome
87where
88 W: Fn(&Path) -> Result<(), String> + Send + Sync + 'static,
89{
90 let (sender, receiver) = std::sync::mpsc::channel();
91 let worker = std::sync::Arc::clone(worker);
92 let path = item.to_path_buf();
93 std::thread::spawn(move || {
96 let _ = sender.send(worker(&path));
97 });
98 match receiver.recv_timeout(timeout) {
99 Ok(Ok(())) => RegenOutcome::Regenerated,
100 Ok(Err(message)) => RegenOutcome::Failed(message),
101 Err(_) => RegenOutcome::TimedOut,
102 }
103}