1#![doc = include_str!("../README.md")]
2
3use std::convert::Infallible;
4
5use axum::{
6 extract::FromRequestParts,
7 http::{
8 header::{
9 HeaderValue, ACCEPT_ENCODING, CACHE_CONTROL, CONTENT_ENCODING, CONTENT_TYPE, ETAG,
10 IF_NONE_MATCH, VARY,
11 },
12 request::Parts,
13 StatusCode,
14 },
15 response::IntoResponse,
16 routing::{get, MethodRouter},
17 Router,
18};
19use bytes::Bytes;
20
21pub use static_serve_macro::{embed_asset, embed_assets};
22
23#[derive(Debug, Copy, Clone)]
25struct AcceptEncoding {
26 pub gzip: bool,
28 pub zstd: bool,
30}
31
32impl<S> FromRequestParts<S> for AcceptEncoding
33where
34 S: Send + Sync,
35{
36 type Rejection = Infallible;
37
38 async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
39 let accept_encoding = parts.headers.get(ACCEPT_ENCODING);
40 let accept_encoding = accept_encoding
41 .and_then(|accept_encoding| accept_encoding.to_str().ok())
42 .unwrap_or_default();
43
44 Ok(Self {
45 gzip: accept_encoding.contains("gzip"),
46 zstd: accept_encoding.contains("zstd"),
47 })
48 }
49}
50
51#[derive(Debug)]
53struct IfNoneMatch(Option<HeaderValue>);
54
55impl IfNoneMatch {
56 fn matches(&self, etag: &str) -> bool {
58 self.0
59 .as_ref()
60 .is_some_and(|if_none_match| if_none_match.as_bytes() == etag.as_bytes())
61 }
62}
63
64impl<S> FromRequestParts<S> for IfNoneMatch
65where
66 S: Send + Sync,
67{
68 type Rejection = Infallible;
69
70 async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
71 let if_none_match = parts.headers.get(IF_NONE_MATCH).cloned();
72 Ok(Self(if_none_match))
73 }
74}
75
76#[doc(hidden)]
77pub fn static_route<S>(
79 router: Router<S>,
80 web_path: &'static str,
81 content_type: &'static str,
82 etag: &'static str,
83 body: &'static [u8],
84 body_gz: Option<&'static [u8]>,
85 body_zst: Option<&'static [u8]>,
86) -> Router<S>
87where
88 S: Clone + Send + Sync + 'static,
89{
90 router.route(
91 web_path,
92 get(
93 move |accept_encoding: AcceptEncoding, if_none_match: IfNoneMatch| async move {
94 static_inner(
95 content_type,
96 etag,
97 body,
98 body_gz,
99 body_zst,
100 accept_encoding,
101 &if_none_match,
102 )
103 },
104 ),
105 )
106}
107
108#[doc(hidden)]
109pub fn static_method_router(
111 content_type: &'static str,
112 etag: &'static str,
113 body: &'static [u8],
114 body_gz: Option<&'static [u8]>,
115 body_zst: Option<&'static [u8]>,
116) -> MethodRouter {
117 MethodRouter::get(
118 MethodRouter::new(),
119 move |accept_encoding: AcceptEncoding, if_none_match: IfNoneMatch| async move {
120 static_inner(
121 content_type,
122 etag,
123 body,
124 body_gz,
125 body_zst,
126 accept_encoding,
127 &if_none_match,
128 )
129 },
130 )
131}
132
133fn static_inner(
134 content_type: &'static str,
135 etag: &'static str,
136 body: &'static [u8],
137 body_gz: Option<&'static [u8]>,
138 body_zst: Option<&'static [u8]>,
139 accept_encoding: AcceptEncoding,
140 if_none_match: &IfNoneMatch,
141) -> impl IntoResponse {
142 let headers_base = [
143 (CONTENT_TYPE, HeaderValue::from_static(content_type)),
144 (ETAG, HeaderValue::from_static(etag)),
145 (
146 CACHE_CONTROL,
147 HeaderValue::from_static("public, max-age=31536000, immutable"),
148 ),
149 (VARY, HeaderValue::from_static("Accept-Encoding")),
150 ];
151
152 match (
153 if_none_match.matches(etag),
154 accept_encoding.gzip,
155 accept_encoding.zstd,
156 body_gz,
157 body_zst,
158 ) {
159 (true, _, _, _, _) => (headers_base, StatusCode::NOT_MODIFIED).into_response(),
160 (false, _, true, _, Some(body_zst)) => (
161 headers_base,
162 [(CONTENT_ENCODING, HeaderValue::from_static("zstd"))],
163 Bytes::from_static(body_zst),
164 )
165 .into_response(),
166 (false, true, _, Some(body_gz), _) => (
167 headers_base,
168 [(CONTENT_ENCODING, HeaderValue::from_static("gzip"))],
169 Bytes::from_static(body_gz),
170 )
171 .into_response(),
172 _ => (headers_base, Bytes::from_static(body)).into_response(),
173 }
174}