webserver_base/assets/
cache_buster.rs1use std::collections::BTreeMap;
8use std::fmt::{self, Display, Formatter};
9use std::path::{Path, PathBuf};
10
11use axum::body::Body;
12use axum::extract::Request;
13use axum::http::header::{
14 CACHE_CONTROL, ETAG, EXPIRES, IF_MATCH, IF_MODIFIED_SINCE, IF_NONE_MATCH, IF_RANGE,
15 IF_UNMODIFIED_SINCE, PRAGMA,
16};
17use axum::http::{HeaderMap, HeaderValue, StatusCode};
18use axum::middleware::Next;
19use axum::response::Response;
20use chrono::{DateTime, Duration, TimeDelta, Utc};
21use tracing::instrument;
22
23use super::error::CacheBusterError;
24use super::generate::STATIC_DIRECTORY;
25use super::manifest::{MANIFEST_PATH, Manifest};
26
27#[derive(Debug, Clone, Default)]
29pub struct CacheBuster {
30 manifest: Manifest,
31}
32
33impl CacheBuster {
34 #[must_use]
37 pub fn empty() -> Self {
38 Self::default()
39 }
40
41 #[instrument(skip_all)]
53 pub fn load() -> Result<Self, CacheBusterError> {
54 if !Path::new(STATIC_DIRECTORY).is_dir() {
55 return Ok(Self::empty());
56 }
57 if !Path::new(MANIFEST_PATH).is_file() {
58 return Err(CacheBusterError::MissingManifest {
59 path: PathBuf::from(MANIFEST_PATH),
60 });
61 }
62 Ok(Self {
63 manifest: Manifest::load()?,
64 })
65 }
66
67 #[must_use]
73 pub fn get_file(&self, original: &str) -> String {
74 self.manifest.resolve(original).to_string()
75 }
76
77 #[must_use]
84 pub fn is_hashed(&self, original: &str) -> bool {
85 self.manifest.contains(original)
86 }
87
88 #[must_use]
90 pub const fn manifest(&self) -> &Manifest {
91 &self.manifest
92 }
93
94 #[must_use]
96 pub const fn cache(&self) -> &BTreeMap<String, String> {
97 self.manifest.entries()
98 }
99
100 #[must_use]
102 pub fn is_empty(&self) -> bool {
103 self.manifest.is_empty()
104 }
105
106 #[instrument(skip_all)]
116 pub async fn never_cache_middleware(
117 request: Request,
118 next: Next,
119 ) -> Result<Response, StatusCode> {
120 let mut response: Response<Body> = next.run(request).await;
121 let headers: &mut HeaderMap = response.headers_mut();
122
123 remove_conditional_headers(headers);
124 headers.insert(
125 EXPIRES,
126 HeaderValue::from_static("Thu, 01 Jan 1970 00:00:00 GMT"),
127 );
128 headers.insert(
129 CACHE_CONTROL,
130 HeaderValue::from_static("no-cache, no-store, must-revalidate, private, max-age=0"),
131 );
132 headers.insert(PRAGMA, HeaderValue::from_static("no-cache"));
133
134 Ok(response)
135 }
136
137 #[instrument(skip_all)]
147 pub async fn forever_cache_middleware(
148 request: Request,
149 next: Next,
150 ) -> Result<Response, StatusCode> {
151 let mut response: Response<Body> = next.run(request).await;
152 let headers: &mut HeaderMap = response.headers_mut();
153
154 remove_conditional_headers(headers);
155
156 let one_year: TimeDelta = Duration::days(365);
157 let expires: DateTime<Utc> = Utc::now() + one_year;
158 if let Ok(expires) = HeaderValue::from_str(&expires.to_rfc2822()) {
159 headers.insert(EXPIRES, expires);
160 }
161 headers.insert(
162 CACHE_CONTROL,
163 HeaderValue::from_static("public, max-age=31536000, must-revalidate, immutable"),
164 );
165
166 Ok(response)
167 }
168}
169
170impl Display for CacheBuster {
171 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
172 write!(f, "CacheBuster ({} entries):", self.manifest.len())?;
173 for (original, hashed) in self.manifest.entries() {
174 write!(f, "\n\t`{original}` -> `{hashed}`")?;
175 }
176 Ok(())
177 }
178}
179
180fn remove_conditional_headers(headers: &mut HeaderMap) {
181 headers.remove(ETAG);
182 headers.remove(IF_MODIFIED_SINCE);
183 headers.remove(IF_MATCH);
184 headers.remove(IF_NONE_MATCH);
185 headers.remove(IF_RANGE);
186 headers.remove(IF_UNMODIFIED_SINCE);
187}
188
189#[cfg(test)]
190mod tests {
191 use super::CacheBuster;
192
193 #[test]
194 fn an_empty_cache_buster_returns_paths_unchanged() {
195 let cache_buster: CacheBuster = CacheBuster::empty();
196
197 let expected: String = String::from("static/stylesheet/main.css");
198 let actual: String = cache_buster.get_file("static/stylesheet/main.css");
199 assert_eq!(expected, actual);
200
201 let expected: bool = true;
202 let actual: bool = cache_buster.is_empty();
203 assert_eq!(expected, actual);
204 }
205}