Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122use std::fs;use std::io;use std::path::{Path, PathBuf};use std::time::{SystemTime, UNIX_EPOCH};
fn feed_path(state_dir: &Path, name: &str) -> PathBuf { state_dir.join(format!("{name}.xml"))}
fn timestamp_path(state_dir: &Path, name: &str) -> PathBuf { state_dir.join(format!("{name}.timestamp"))}
fn validators_path(state_dir: &Path, name: &str) -> PathBuf { state_dir.join(format!("{name}.validators"))}
/// The conditional-request validators an origin server sent with a site's/// last successful fetch, so the next fetch can ask "has this changed?"/// instead of downloading the page again regardless.#[derive(Debug, Default, Clone, PartialEq, Eq)]pub struct Validators { pub etag: Option<String>, pub last_modified: Option<String>,}
/// Writes `path` by writing to a `.tmp` sibling and renaming over the/// destination, so a reader never observes a partially-written file.fn atomic_write(path: &Path, contents: &[u8]) -> io::Result<()> { let mut tmp = path.as_os_str().to_owned(); tmp.push(".tmp"); let tmp_path = PathBuf::from(tmp); fs::write(&tmp_path, contents)?; fs::rename(&tmp_path, path)}
pub fn write_feed(state_dir: &Path, name: &str, xml: &str) -> io::Result<()> { fs::create_dir_all(state_dir)?; atomic_write(&feed_path(state_dir, name), xml.as_bytes())?; let now = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs(); atomic_write(×tamp_path(state_dir, name), now.to_string().as_bytes())}
pub fn read_feed(state_dir: &Path, name: &str) -> io::Result<String> { fs::read_to_string(feed_path(state_dir, name))}
pub fn read_timestamp(state_dir: &Path, name: &str) -> Option<u64> { fs::read_to_string(timestamp_path(state_dir, name)) .ok()? .trim() .parse() .ok()}
/// Persists the validators from a site's latest response so the next fetch/// can send them back as `If-None-Match`/`If-Modified-Since`. Called/// unconditionally after every successful (non-304) fetch, including when a/// server stops sending a validator it previously sent -- the stored value/// should track what the server currently supports.pub fn write_validators(state_dir: &Path, name: &str, validators: &Validators) -> io::Result<()> { fs::create_dir_all(state_dir)?; let contents = format!( "{}\n{}\n", validators.etag.as_deref().unwrap_or(""), validators.last_modified.as_deref().unwrap_or(""), ); atomic_write(&validators_path(state_dir, name), contents.as_bytes())}
pub fn read_validators(state_dir: &Path, name: &str) -> Validators { let contents = fs::read_to_string(validators_path(state_dir, name)).unwrap_or_default(); let mut lines = contents.lines(); let non_empty = |s: &str| (!s.is_empty()).then(|| s.to_string()); Validators { etag: lines.next().and_then(non_empty), last_modified: lines.next().and_then(non_empty), }}
#[cfg(test)]mod tests { use super::*;
fn scratch_dir(name: &str) -> PathBuf { let dir = std::env::temp_dir().join(format!("luafeed-cache-test-{name}")); let _ = fs::remove_dir_all(&dir); dir }
#[test] fn missing_validators_file_reads_as_all_none() { let dir = scratch_dir("missing-validators"); assert_eq!(read_validators(&dir, "site"), Validators::default()); }
#[test] fn validators_round_trip_through_write_and_read() { let dir = scratch_dir("validators-round-trip"); let validators = Validators { etag: Some("\"abc123\"".to_string()), last_modified: Some("Sun, 06 Sep 2026 00:00:00 GMT".to_string()), }; write_validators(&dir, "site", &validators).unwrap(); assert_eq!(read_validators(&dir, "site"), validators); }
#[test] fn validators_round_trip_with_only_one_field_present() { let dir = scratch_dir("validators-partial"); let validators = Validators { etag: Some("\"abc123\"".to_string()), last_modified: None, }; write_validators(&dir, "site", &validators).unwrap(); assert_eq!(read_validators(&dir, "site"), validators); }}