systemprompt_api/services/static_content/static_files/
cache.rs1use axum::http::{HeaderMap, StatusCode, header};
14use axum::response::IntoResponse;
15use std::hash::{Hash, Hasher};
16use std::path::Path;
17
18pub const CACHE_STATIC_ASSET: &str = "public, max-age=31536000, immutable";
19pub const CACHE_STATIC_ASSET_REVALIDATE: &str = "public, max-age=0, must-revalidate";
20pub const CACHE_HTML: &str = "no-cache";
21pub const CACHE_METADATA: &str = "public, max-age=3600";
22
23const CONTENT_HASH_MIN_LEN: usize = 8;
24const CONTENT_HASH_MAX_LEN: usize = 32;
25
26pub fn asset_cache_policy(path: &Path) -> &'static str {
27 let hashed = path
28 .file_name()
29 .and_then(|name| name.to_str())
30 .is_some_and(has_content_hash_segment);
31
32 if hashed {
33 CACHE_STATIC_ASSET
34 } else {
35 CACHE_STATIC_ASSET_REVALIDATE
36 }
37}
38
39fn has_content_hash_segment(file_name: &str) -> bool {
40 let segments: Vec<&str> = file_name.split(['.', '-']).collect();
41 let Some(interior) = segments.get(1..segments.len().saturating_sub(1)) else {
42 return false;
43 };
44 interior.iter().copied().any(is_content_hash)
45}
46
47fn is_content_hash(segment: &str) -> bool {
48 if !(CONTENT_HASH_MIN_LEN..=CONTENT_HASH_MAX_LEN).contains(&segment.len())
49 || !segment.chars().all(|c| c.is_ascii_alphanumeric())
50 {
51 return false;
52 }
53 let all_hex = segment.chars().all(|c| c.is_ascii_hexdigit());
54 let mixed = segment.chars().any(|c| c.is_ascii_digit())
55 && segment.chars().any(|c| c.is_ascii_alphabetic());
56 all_hex || mixed
57}
58
59pub fn compute_etag(content: &[u8]) -> String {
60 let mut hasher = std::collections::hash_map::DefaultHasher::new();
61 content.hash(&mut hasher);
62 format!("\"{}\"", hasher.finish())
63}
64
65pub(super) fn etag_matches(headers: &HeaderMap, etag: &str) -> bool {
66 headers
67 .get(header::IF_NONE_MATCH)
68 .and_then(|v| v.to_str().ok())
69 == Some(etag)
70}
71
72pub(super) fn not_modified_response(etag: &str, cache_control: &str) -> axum::response::Response {
73 (
74 StatusCode::NOT_MODIFIED,
75 [
76 (header::ETAG, etag.to_owned()),
77 (header::CACHE_CONTROL, cache_control.to_owned()),
78 ],
79 )
80 .into_response()
81}
82
83fn serve_file_response(
84 content: Vec<u8>,
85 content_type: String,
86 cache_control: &str,
87 etag: String,
88) -> axum::response::Response {
89 (
90 StatusCode::OK,
91 [
92 (header::CONTENT_TYPE, content_type),
93 (header::CACHE_CONTROL, cache_control.to_owned()),
94 (header::ETAG, etag),
95 ],
96 content,
97 )
98 .into_response()
99}
100
101pub(super) async fn serve_cached_file(
102 file_path: &Path,
103 headers: &HeaderMap,
104 content_type: &str,
105 cache_control: &str,
106) -> axum::response::Response {
107 match tokio::fs::read(file_path).await {
108 Ok(content) => {
109 let etag = compute_etag(&content);
110 if etag_matches(headers, &etag) {
111 return not_modified_response(&etag, cache_control);
112 }
113 serve_file_response(content, content_type.to_owned(), cache_control, etag)
114 },
115 Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Error reading file").into_response(),
116 }
117}