actix_web_rust_embed_responder/
embed.rs1use actix_web::{
2 body::{BoxBody, MessageBody},
3 http::Method,
4 HttpRequest, HttpResponse, Responder,
5};
6
7#[cfg(feature = "compression-zstd")]
8use crate::compress_data_zstd;
9use crate::{
10 compress::Compress, compress_data_br, compress_data_gzip, helper::accepts_encoding,
11 is_well_known_compressible_mime_type, parse::parse_if_none_match_value,
12};
13
14pub trait EmbedRespondable {
21 type Data: MessageBody + 'static + AsRef<[u8]>;
22 type DataGzip: MessageBody + 'static + AsRef<[u8]>;
23 type DataBr: MessageBody + 'static + AsRef<[u8]>;
24 type DataZstd: MessageBody + 'static + AsRef<[u8]>;
25 type MimeType: AsRef<str>;
26 type ETag: AsRef<str>;
27 type LastModified: AsRef<str>;
28
29 fn data(&self) -> Self::Data;
31 fn data_gzip(&self) -> Option<Self::DataGzip>;
35 fn data_br(&self) -> Option<Self::DataBr>;
39 fn data_zstd(&self) -> Option<Self::DataZstd>;
43 fn last_modified_timestamp(&self) -> Option<i64>;
45 fn last_modified(&self) -> Option<Self::LastModified>;
47 fn etag(&self) -> Self::ETag;
49 fn mime_type(&self) -> Option<Self::MimeType>;
51}
52
53pub struct EmbedResponse<T: EmbedRespondable> {
58 pub(crate) file: Option<T>,
59 pub(crate) compress: Compress,
60}
61
62enum ShouldCompress {
63 Zstd,
64 Brotli,
65 Gzip,
66 No,
67}
68
69fn should_compress<T: EmbedRespondable>(
70 req: &HttpRequest,
71 file: &T,
72 compress: &Compress,
73) -> ShouldCompress {
74 let should_compress_for_encoding =
75 |is_precompressed_for_encoding: bool, mime_type: Option<T::MimeType>, encoding: &str| {
76 accepts_encoding(req, encoding)
77 && match compress {
78 Compress::Never => false,
79 Compress::IfPrecompressed => is_precompressed_for_encoding,
80 Compress::IfWellKnown => mime_type
81 .map(|v| is_well_known_compressible_mime_type(v.as_ref()))
82 .unwrap_or(false),
83 Compress::Always => true,
84 }
85 };
86
87 if should_compress_for_encoding(file.data_zstd().is_some(), file.mime_type(), "zstd") {
88 ShouldCompress::Zstd
89 } else if should_compress_for_encoding(file.data_br().is_some(), file.mime_type(), "br") {
90 ShouldCompress::Brotli
91 } else if should_compress_for_encoding(file.data_gzip().is_some(), file.mime_type(), "gzip") {
92 ShouldCompress::Gzip
93 } else {
94 ShouldCompress::No
95 }
96}
97
98fn send_response<T: EmbedRespondable>(
99 req: &HttpRequest,
100 file: &T,
101 compress: Compress,
102) -> HttpResponse {
103 let mut resp = HttpResponse::Ok();
104
105 resp.append_header(("ETag", file.etag().as_ref()));
106 if let Some(last_modified) = file.last_modified() {
107 resp.append_header(("Last-Modified", last_modified.as_ref()));
108 }
109 if let Some(mime_type) = file.mime_type() {
110 resp.append_header(("Content-Type", mime_type.as_ref()));
111 }
112
113 resp.append_header(("Cache-Control", "no-cache"));
119
120 if req.method() == Method::HEAD {
121 resp.finish()
123 } else {
124 let encoding_choice = should_compress(req, file, &compress);
128 match encoding_choice {
129 #[cfg(feature = "compression-zstd")]
130 ShouldCompress::Zstd => {
131 resp.append_header(("Content-Encoding", "zstd"));
132 match file.data_zstd() {
133 Some(data_zstd) => resp.body(data_zstd),
134 None => resp.body(compress_data_zstd(
135 file.etag().as_ref(),
136 file.data().as_ref(),
137 )),
138 }
139 }
140 ShouldCompress::Brotli => {
141 resp.append_header(("Content-Encoding", "br"));
142 match file.data_br() {
143 Some(data_br) => resp.body(data_br),
144 None => resp.body(compress_data_br(file.etag().as_ref(), file.data().as_ref())),
145 }
146 }
147 ShouldCompress::Gzip => {
148 resp.append_header(("Content-Encoding", "gzip"));
149 match file.data_gzip() {
150 Some(data_gzip) => resp.body(data_gzip),
151 None => resp.body(compress_data_gzip(
152 file.etag().as_ref(),
153 file.data().as_ref(),
154 )),
155 }
156 }
157 #[cfg(not(feature = "compression-zstd"))]
158 ShouldCompress::Zstd => {
159 resp.body(file.data())
161 }
162 ShouldCompress::No => resp.body(file.data()),
163 }
164 }
165}
166
167impl<T: EmbedRespondable> Responder for EmbedResponse<T> {
168 type Body = BoxBody;
169
170 fn respond_to(self, req: &HttpRequest) -> HttpResponse<Self::Body> {
171 match self.file {
172 Some(file) => {
173 if req.method() != Method::GET && req.method() != Method::HEAD {
175 return HttpResponse::NotImplemented().finish();
176 }
177
178 let e = file.etag();
181 let etag = e.as_ref();
182
183 let last_modified_timestamp = file.last_modified_timestamp();
184
185 if let Some(req_etags) = req
192 .headers()
193 .get("If-None-Match")
194 .and_then(parse_if_none_match_value)
195 {
196 if req_etags.contains(&etag) {
197 return HttpResponse::NotModified().finish();
198 } else {
199 return send_response(req, &file, self.compress);
200 }
201 }
202 if let Some(last_modified_timestamp) = last_modified_timestamp {
207 if let Some(if_unmodified_since) = req
208 .headers()
209 .get("If-Unmodified-Since")
210 .and_then(|v| v.to_str().ok())
211 .and_then(|v| chrono::DateTime::parse_from_rfc2822(v).ok())
212 {
213 if last_modified_timestamp > if_unmodified_since.timestamp() {
215 return send_response(req, &file, self.compress);
216 } else {
217 return HttpResponse::NotModified().finish();
218 }
219 }
220 }
221 send_response(req, &file, self.compress)
224 }
225 None => HttpResponse::NotFound().finish(),
226 }
227 }
228}
229
230impl<T: EmbedRespondable> EmbedResponse<T> {
231 pub fn use_compression(mut self, option: Compress) -> Self {
234 self.compress = option;
235 self
236 }
237}
238
239pub trait IntoResponse<T: EmbedRespondable> {
241 fn into_response(self) -> EmbedResponse<T>;
243}