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    content_modified_in(Path::new(""), directories)
30}
31
32/// [`content_modified`], with `directories` under `root`.
33pub(crate) fn content_modified_in(root: &Path, directories: &[&str]) -> Option<DateTime<Utc>> {
34    let newest: DateTime<Utc> = directories
35        .iter()
36        .filter_map(|directory| newest_mtime(&root.join(directory)))
37        .chain(current_exe_mtime())
38        .max()?;
39
40    let now: DateTime<Utc> = Utc::now();
41    if newest.year() <= 2000 || newest > now {
42        error!(
43            "content modification time is `{newest}`, which is implausible; \
44             omitting <lastmod>. Check that the build preserves file timestamps."
45        );
46        return None;
47    }
48
49    Some(newest)
50}
51
52/// The newest mtime anywhere under `root`.
53fn newest_mtime(root: &Path) -> Option<DateTime<Utc>> {
54    if !root.is_dir() {
55        return None;
56    }
57
58    let mut newest: Option<DateTime<Utc>> = None;
59    let mut directories: Vec<std::path::PathBuf> = vec![root.to_path_buf()];
60
61    while let Some(directory) = directories.pop() {
62        let Ok(entries) = std::fs::read_dir(&directory) else {
63            continue;
64        };
65        for entry in entries.flatten() {
66            let path: PathBuf = entry.path();
67            if path.is_dir() {
68                directories.push(path);
69                continue;
70            }
71            let Ok(modified) = entry.metadata().and_then(|meta| meta.modified()) else {
72                continue;
73            };
74            let modified: DateTime<Utc> = modified.into();
75            if newest.is_none_or(|current| modified > current) {
76                newest = Some(modified);
77            }
78        }
79    }
80
81    newest
82}
83
84/// The running binary's mtime, so a code-only change still moves the date.
85fn current_exe_mtime() -> Option<DateTime<Utc>> {
86    let exe: PathBuf = std::env::current_exe().ok()?;
87    let modified: SystemTime = std::fs::metadata(exe).ok()?.modified().ok()?;
88    Some(modified.into())
89}
90
91#[cfg(test)]
92mod tests {
93    use chrono::{DateTime, Utc};
94
95    use super::{content_modified, newest_mtime};
96
97    #[test]
98    fn a_directory_that_does_not_exist_contributes_nothing() {
99        let expected: Option<DateTime<Utc>> = None;
100        let actual: Option<DateTime<Utc>> =
101            newest_mtime(std::path::Path::new("/tmp/wsb-no-such-dir"));
102        assert_eq!(expected, actual);
103    }
104
105    #[test]
106    fn the_binarys_own_timestamp_is_enough_to_produce_a_plausible_date() {
107        // Even with no content directories, the test binary itself is recent,
108        // so this must not fall through to the implausible branch.
109        let actual: Option<DateTime<Utc>> = content_modified(&["/tmp/wsb-no-such-dir"]);
110        assert!(actual.is_some());
111    }
112}