1use axum::http::{HeaderMap, StatusCode};
2use chrono::TimeDelta;
3use std::pin::Pin;
4
5use crate::{RenderContext, Rendered, RenderedBody, mime::MimeType, servable::Servable};
6
7pub struct StaticAsset {
9 pub bytes: &'static [u8],
11
12 pub mime: MimeType,
14
15 pub ttl: Option<TimeDelta>,
18}
19
20impl StaticAsset {
21 pub const DEFAULT_TTL: Option<TimeDelta> = Some(TimeDelta::days(14));
23
24 pub const fn with_ttl(mut self, ttl: Option<TimeDelta>) -> Self {
26 self.ttl = ttl;
27 self
28 }
29}
30
31#[cfg(feature = "image")]
32impl Servable for StaticAsset {
33 fn head<'a>(
34 &'a self,
35 ctx: &'a RenderContext,
36 ) -> Pin<Box<dyn Future<Output = Rendered<()>> + 'a + Send + Sync>> {
37 Box::pin(async {
38 use crate::transform::TransformerChain;
39 use std::str::FromStr;
40
41 let is_image = TransformerChain::mime_is_image(&self.mime);
42
43 let transform = match (is_image, ctx.query.get("t")) {
44 (false, _) | (_, None) => None,
45
46 (true, Some(x)) => match TransformerChain::from_str(x) {
47 Ok(x) => Some(x),
48 Err(_err) => {
49 return Rendered {
50 code: StatusCode::BAD_REQUEST,
51 body: (),
52 ttl: self.ttl,
53 private: false,
54
55 headers: HeaderMap::new(),
56 mime: None,
57 };
58 }
59 },
60 };
61
62 match transform {
63 Some(transform) => {
64 return Rendered {
65 code: StatusCode::OK,
66 body: (),
67 ttl: self.ttl,
68 private: false,
69
70 headers: HeaderMap::new(),
71 mime: Some(
72 transform
73 .output_mime(&self.mime)
74 .unwrap_or(self.mime.clone()),
75 ),
76 };
77 }
78
79 None => {
80 return Rendered {
81 code: StatusCode::OK,
82 body: (),
83 ttl: self.ttl,
84 private: false,
85
86 headers: HeaderMap::new(),
87 mime: Some(self.mime.clone()),
88 };
89 }
90 }
91 })
92 }
93
94 fn render<'a>(
95 &'a self,
96 ctx: &'a RenderContext,
97 ) -> Pin<Box<dyn Future<Output = Rendered<RenderedBody>> + 'a + Send + Sync>> {
98 Box::pin(async {
99 use crate::transform::TransformerChain;
100 use std::str::FromStr;
101 use tracing::{error, trace};
102
103 let is_image = TransformerChain::mime_is_image(&self.mime);
105
106 let transform = match (is_image, ctx.query.get("t")) {
107 (false, _) | (_, None) => None,
108
109 (true, Some(x)) => match TransformerChain::from_str(x) {
110 Ok(x) => Some(x),
111 Err(err) => {
112 return Rendered {
113 code: StatusCode::BAD_REQUEST,
114 body: RenderedBody::String(err),
115 ttl: self.ttl,
116 private: false,
117
118 headers: HeaderMap::new(),
119 mime: None,
120 };
121 }
122 },
123 };
124
125 match transform {
126 Some(transform) => {
127 trace!(message = "Transforming image", ?transform);
128
129 let task = {
130 let mime = Some(self.mime.clone());
131 let bytes = self.bytes;
132 tokio::task::spawn_blocking(move || {
133 transform.transform_bytes(bytes, mime.as_ref())
134 })
135 };
136
137 let res = match task.await {
138 Ok(x) => x,
139 Err(error) => {
140 error!(message = "Error while transforming image", ?error);
141 return Rendered {
142 code: StatusCode::INTERNAL_SERVER_ERROR,
143 body: RenderedBody::String(format!(
144 "Error while transforming image: {error:?}"
145 )),
146 ttl: None,
147 private: false,
148
149 headers: HeaderMap::new(),
150 mime: None,
151 };
152 }
153 };
154
155 match res {
156 Ok((mime, bytes)) => {
157 return Rendered {
158 code: StatusCode::OK,
159 body: RenderedBody::Bytes(bytes),
160 ttl: self.ttl,
161 private: false,
162
163 headers: HeaderMap::new(),
164 mime: Some(mime),
165 };
166 }
167
168 Err(err) => {
169 return Rendered {
170 code: StatusCode::INTERNAL_SERVER_ERROR,
171 body: RenderedBody::String(format!("{err}")),
172 ttl: self.ttl,
173 private: false,
174
175 headers: HeaderMap::new(),
176 mime: None,
177 };
178 }
179 }
180 }
181
182 None => {
183 return Rendered {
184 code: StatusCode::OK,
185 body: RenderedBody::Static(self.bytes),
186 ttl: self.ttl,
187 private: false,
188
189 headers: HeaderMap::new(),
190 mime: Some(self.mime.clone()),
191 };
192 }
193 }
194 })
195 }
196}
197
198#[cfg(not(feature = "image"))]
199impl Servable for StaticAsset {
200 fn head<'a>(
201 &'a self,
202 _ctx: &'a RenderContext,
203 ) -> Pin<Box<dyn Future<Output = Rendered<()>> + 'a + Send + Sync>> {
204 Box::pin(async {
205 return Rendered {
206 code: StatusCode::OK,
207 body: (),
208 ttl: self.ttl,
209 private: false,
210
211 headers: HeaderMap::new(),
212 mime: Some(self.mime.clone()),
213 };
214 })
215 }
216
217 fn render<'a>(
218 &'a self,
219 ctx: &'a RenderContext,
220 ) -> Pin<Box<dyn Future<Output = Rendered<RenderedBody>> + 'a + Send + Sync>> {
221 Box::pin(async {
222 self.head(ctx)
223 .await
224 .with_body(RenderedBody::Static(self.bytes))
225 })
226 }
227}