Skip to main content

webserver_base/assets/
modified.rs

1//! When the site's content last changed.
2//!
3//! This feeds `<lastmod>`. Google uses that value only when it is
4//! "consistently and verifiably accurate" — it fetches the page and compares —
5//! and one bad pattern discredits the tag across the whole file. So the number
6//! has to mean something.
7//!
8//! Server *boot* time does not: a restart with no content change would
9//! re-declare the entire site as modified. The binary's own timestamp does not
10//! either, because templates are loaded from disk and ship in a different image
11//! layer, so editing a page leaves the binary untouched. The newest timestamp
12//! across the templates, the static assets *and* the binary moves when — and
13//! only when — something that determines the output actually changed.
14
15use std::path::{Path, PathBuf};
16use std::time::SystemTime;
17
18use chrono::{DateTime, Datelike, Utc};
19use tracing::error;
20
21/// The newest modification time across everything that determines page output.
22///
23/// Returns `None` when the result is implausible, in which case the caller
24/// should omit `<lastmod>` rather than assert something a crawler can falsify.
25/// Reproducible builds normalise timestamps to the epoch, and a skewed clock
26/// can produce a future date; both are worse than saying nothing.
27#[must_use]
28pub fn content_modified(directories: &[&str]) -> Option<DateTime<Utc>> {
29    let newest: DateTime<Utc> = directories
30        .iter()
31        .filter_map(|directory| newest_mtime(Path::new(directory)))
32        .chain(current_exe_mtime())
33        .max()?;
34
35    let now: DateTime<Utc> = Utc::now();
36    if newest.year() <= 2000 || newest > now {
37        error!(
38            "content modification time is `{newest}`, which is implausible; \
39             omitting <lastmod>. Check that the build preserves file timestamps."
40        );
41        return None;
42    }
43
44    Some(newest)
45}
46
47/// The newest mtime anywhere under `root`.
48fn newest_mtime(root: &Path) -> Option<DateTime<Utc>> {
49    if !root.is_dir() {
50        return None;
51    }
52
53    let mut newest: Option<DateTime<Utc>> = None;
54    let mut directories: Vec<std::path::PathBuf> = vec![root.to_path_buf()];
55
56    while let Some(directory) = directories.pop() {
57        let Ok(entries) = std::fs::read_dir(&directory) else {
58            continue;
59        };
60        for entry in entries.flatten() {
61            let path: PathBuf = entry.path();
62            if path.is_dir() {
63                directories.push(path);
64                continue;
65            }
66            let Ok(modified) = entry.metadata().and_then(|meta| meta.modified()) else {
67                continue;
68            };
69            let modified: DateTime<Utc> = modified.into();
70            if newest.is_none_or(|current| modified > current) {
71                newest = Some(modified);
72            }
73        }
74    }
75
76    newest
77}
78
79/// The running binary's mtime, so a code-only change still moves the date.
80fn current_exe_mtime() -> Option<DateTime<Utc>> {
81    let exe: PathBuf = std::env::current_exe().ok()?;
82    let modified: SystemTime = std::fs::metadata(exe).ok()?.modified().ok()?;
83    Some(modified.into())
84}
85
86#[cfg(test)]
87mod tests {
88    use chrono::{DateTime, Utc};
89
90    use super::{content_modified, newest_mtime};
91
92    #[test]
93    fn a_directory_that_does_not_exist_contributes_nothing() {
94        let expected: Option<DateTime<Utc>> = None;
95        let actual: Option<DateTime<Utc>> =
96            newest_mtime(std::path::Path::new("/tmp/wsb-no-such-dir"));
97        assert_eq!(expected, actual);
98    }
99
100    #[test]
101    fn the_binarys_own_timestamp_is_enough_to_produce_a_plausible_date() {
102        // Even with no content directories, the test binary itself is recent,
103        // so this must not fall through to the implausible branch.
104        let actual: Option<DateTime<Utc>> = content_modified(&["/tmp/wsb-no-such-dir"]);
105        assert!(actual.is_some());
106    }
107}