use std::{
env, fs::{self, File}, io::{self, BufReader}, path::{Path, PathBuf}, sync::{Arc, RwLock}, thread::{self, JoinHandle}, time::{Duration, SystemTime},
};
use sha2::Digest;
use crate::{Blog, friendly, live::ProvidedConfig, raw};
pub struct Current {
pub version: Version,
pub blog: io::Result<Blog>,
}
impl ProvidedConfig {
fn path(&self) -> Option<&Path> {
match self {
Self::Live(p) => Some(p),
Self::Static(_) => None,
}
}
fn load(&self) -> io::Result<raw::Config> {
match self {
Self::Live(path) => {
let file = File::open(path)?;
let friendly: friendly::Config = yaml_serde::from_reader(BufReader::new(file))
.map_err(|e| io::Error::other(format!("invalid config: {e}")))?;
let raw = friendly.try_into()
.map_err(|e| io::Error::other(format!("invalid config: {e}")))?;
Ok(raw)
}
Self::Static(cfg) => Ok(cfg.clone()),
}
}
}
pub fn once(cfg: ProvidedConfig, dir: impl Into<PathBuf>) -> Current {
let dir = dir.into();
Current {
version: hash_inputs(cfg.path(), &dir),
blog: cfg.load().and_then(|c| c.build(&dir)),
}
}
pub fn watch(
cfg: ProvidedConfig,
dir: impl Into<PathBuf>,
) -> (Arc<RwLock<Current>>, JoinHandle<()>) {
let dir = dir.into();
let mut version = hash_inputs(cfg.path(), &dir);
let res_dest = Arc::new(RwLock::new(Current {
version,
blog: cfg.load().and_then(|c| c.build(&dir)),
}));
let dest = Arc::downgrade(&res_dest);
let worker = thread::spawn(move || {
loop {
let Some(dest) = dest.upgrade() else {
break;
};
thread::sleep(Duration::from_millis(200));
let new = hash_inputs(cfg.path(), &dir);
if version == new {
continue;
}
version = new;
let blog = cfg.load().and_then(|c| c.build(&dir));
*dest.write().unwrap() = Current {
version,
blog,
};
}
});
(res_dest, worker)
}
pub type Version = u64;
type Algorithm = sha2::Sha256;
fn hash_inputs(cfg: Option<&Path>, dir: &Path) -> Version {
let mut hasher = Algorithm::new();
if let Ok(exe) = env::current_exe() {
add_file(&mut hasher, &exe);
}
if let Some(cfg) = cfg {
add_file(&mut hasher, cfg);
}
add_dir(&mut hasher, dir);
let digest = hasher.finalize();
const BYTES: usize = Version::BITS as usize / 8;
assert!(digest.len() >= BYTES);
Version::from_ne_bytes(digest[..BYTES].try_into().unwrap())
}
fn add_path(hasher: &mut Algorithm, path: &Path) {
let bytes = path.as_os_str().as_encoded_bytes();
hasher.update(bytes.len().to_ne_bytes());
hasher.update(bytes);
}
fn add_dir(hasher: &mut Algorithm, dir: &Path) {
let Ok(rd) = fs::read_dir(dir) else {
return;
};
let mut entries: Vec<_> = rd
.into_iter()
.filter_map(|de| de.ok())
.filter_map(|de| de.file_type().ok().map(|ft| (ft, de.path())))
.collect();
entries.sort_by(|(_, lp), (_, rp)| lp.cmp(rp));
for (ftype, path) in entries {
add_path(hasher, &path);
if ftype.is_file() {
add_file(hasher, &path);
} else if ftype.is_dir() {
add_dir(hasher, &path);
} else {
hasher.update(b"non-file, non-dir. /shrug");
}
}
}
fn add_file(hasher: &mut Algorithm, file: &Path) {
let Ok(metadata) = file.metadata() else {
return;
};
hasher.update(metadata.len().to_ne_bytes());
let Ok(mtime) = metadata.modified() else {
return;
};
match mtime.duration_since(SystemTime::UNIX_EPOCH) {
Ok(ut) => hasher.update(ut.as_millis().to_ne_bytes()),
Err(_) => hasher.update(b"some other time?"),
}
}