servable/servable/
asset.rs

1use axum::http::{HeaderMap, StatusCode};
2use chrono::TimeDelta;
3use std::pin::Pin;
4
5use crate::{RenderContext, Rendered, RenderedBody, mime::MimeType, servable::Servable};
6
7/// A static blob of bytes
8pub struct StaticAsset {
9	/// The data to return
10	pub bytes: &'static [u8],
11
12	/// The type of `bytes`
13	pub mime: MimeType,
14
15	/// How long to cache this response.
16	/// If None, never cache
17	pub ttl: Option<TimeDelta>,
18}
19
20impl StaticAsset {
21	/// Default ttl of a [StaticAsset]
22	pub const DEFAULT_TTL: Option<TimeDelta> = Some(TimeDelta::days(14));
23
24	/// Set `self.ttl`
25	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			// Automatically provide transformation if this is an image
104			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}