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