Skip to main content

systemprompt_api/services/static_content/static_files/
cache.rs

1//! Cache-control constants and `ETag` computation/matching for static file
2//! responses.
3//!
4//! Copyright (c) systemprompt.io — Business Source License 1.1.
5//! See <https://systemprompt.io> for licensing details.
6
7use axum::http::{HeaderMap, StatusCode, header};
8use axum::response::IntoResponse;
9use std::hash::{Hash, Hasher};
10
11pub const CACHE_STATIC_ASSET: &str = "public, max-age=31536000, immutable";
12pub const CACHE_HTML: &str = "no-cache";
13pub const CACHE_METADATA: &str = "public, max-age=3600";
14
15pub fn compute_etag(content: &[u8]) -> String {
16    let mut hasher = std::collections::hash_map::DefaultHasher::new();
17    content.hash(&mut hasher);
18    format!("\"{}\"", hasher.finish())
19}
20
21pub(super) fn etag_matches(headers: &HeaderMap, etag: &str) -> bool {
22    headers
23        .get(header::IF_NONE_MATCH)
24        .and_then(|v| v.to_str().ok())
25        == Some(etag)
26}
27
28pub(super) fn not_modified_response(
29    etag: &str,
30    cache_control: &'static str,
31) -> axum::response::Response {
32    (
33        StatusCode::NOT_MODIFIED,
34        [
35            (header::ETAG, etag.to_owned()),
36            (header::CACHE_CONTROL, cache_control.to_owned()),
37        ],
38    )
39        .into_response()
40}
41
42fn serve_file_response(
43    content: Vec<u8>,
44    content_type: String,
45    cache_control: &'static str,
46    etag: String,
47) -> axum::response::Response {
48    (
49        StatusCode::OK,
50        [
51            (header::CONTENT_TYPE, content_type),
52            (header::CACHE_CONTROL, cache_control.to_owned()),
53            (header::ETAG, etag),
54        ],
55        content,
56    )
57        .into_response()
58}
59
60pub(super) async fn serve_cached_file(
61    file_path: &std::path::Path,
62    headers: &HeaderMap,
63    content_type: &str,
64    cache_control: &'static str,
65) -> axum::response::Response {
66    match tokio::fs::read(file_path).await {
67        Ok(content) => {
68            let etag = compute_etag(&content);
69            if etag_matches(headers, &etag) {
70                return not_modified_response(&etag, cache_control);
71            }
72            serve_file_response(content, content_type.to_owned(), cache_control, etag)
73        },
74        Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Error reading file").into_response(),
75    }
76}