bbox_core/
static_files.rs1use actix_web::{
2 body::BoxBody,
3 http::{header, StatusCode},
4 web, Error, HttpMessage, HttpRequest, HttpResponse, Responder,
5};
6use rust_embed::RustEmbed;
7use std::cell::RefCell;
8use std::collections::{BTreeMap, HashMap};
9use std::io;
10use std::path::{Path, PathBuf};
11
12#[derive(RustEmbed)]
13#[folder = "src/empty/"]
14pub struct EmptyDir;
15
16pub async fn embedded<E: RustEmbed>(path: web::Path<PathBuf>) -> Result<EmbedFile, Error> {
18 Ok(EmbedFile::open::<E, _>(path.as_ref())?)
19}
20
21pub async fn embedded_index<E: RustEmbed>(path: web::Path<PathBuf>) -> Result<EmbedFile, Error> {
23 let filename = if path.as_ref() == &PathBuf::from("") {
24 PathBuf::from("index.html")
25 } else {
26 path.to_path_buf()
27 };
28 Ok(EmbedFile::open::<E, _>(filename)?)
29}
30
31type EtagMap = HashMap<&'static str, BTreeMap<String, u64>>;
32
33thread_local! {
37 static ETAG: RefCell<EtagMap> = init();
38}
39
40fn init() -> RefCell<EtagMap> {
41 RefCell::new(EtagMap::new())
42}
43
44fn get_etag<E>(filename: &str) -> Option<u64>
45where
46 E: RustEmbed,
47{
48 let filename = filename.to_string();
49 let typename = std::any::type_name::<E>();
50 ETAG.with(|m| {
51 if let Some(map) = m.borrow().get(typename) {
52 return map.get(&filename).copied();
53 }
54 let map = init_etag::<E>();
55 let r = map.get(&filename).copied();
56 m.borrow_mut().insert(typename, map);
57 r
58 })
59}
60
61fn init_etag<E>() -> BTreeMap<String, u64>
62where
63 E: RustEmbed,
64{
65 let mut map = BTreeMap::new();
66 for file in E::iter() {
67 let file = file.as_ref();
68 let etag = match E::get(file).map(|c| fxhash::hash64(&c.data)) {
69 Some(etag) => etag,
70 None => continue,
71 };
72 map.insert(file.into(), etag);
73 }
74 map
75}
76
77fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
79 match req.get_header::<header::IfNoneMatch>() {
80 Some(header::IfNoneMatch::Any) => false,
81
82 Some(header::IfNoneMatch::Items(ref items)) => {
83 if let Some(some_etag) = etag {
84 for item in items {
85 if item.weak_eq(some_etag) {
86 return false;
87 }
88 }
89 }
90
91 true
92 }
93
94 None => true,
95 }
96}
97
98fn io_not_found<S>(info: S) -> io::Error
99where
100 S: AsRef<str>,
101{
102 io::Error::new(io::ErrorKind::NotFound, info.as_ref())
103}
104
105pub struct EmbedFile {
106 content: Vec<u8>,
107 content_type: mime::Mime,
108 etag: Option<header::EntityTag>,
109}
110
111impl EmbedFile {
112 pub fn open<E, P>(path: P) -> io::Result<EmbedFile>
113 where
114 E: RustEmbed,
115 P: AsRef<Path>,
116 {
117 let mut path = path.as_ref();
118 while let Ok(new_path) = path.strip_prefix(".") {
119 path = new_path;
120 }
121 Self::open_impl::<E>(path).ok_or(io_not_found("File not found"))
122 }
123
124 fn open_impl<E>(path: &Path) -> Option<EmbedFile>
125 where
126 E: RustEmbed,
127 {
128 let content_type = mime_guess::from_path(path).first_or_octet_stream();
129 let filename = path.to_str()?;
130 let etag = get_etag::<E>(filename);
131 let r = EmbedFile {
132 content: E::get(filename)?.data.to_vec(),
133 content_type,
134 etag: etag.map(|etag| header::EntityTag::new_strong(format!("{:x}", etag))),
135 };
136 Some(r)
137 }
138
139 fn into_response(self, req: &HttpRequest) -> HttpResponse {
140 let status_code = if !none_match(self.etag.as_ref(), req) {
141 StatusCode::NOT_MODIFIED
142 } else {
143 StatusCode::OK
144 };
145
146 let mut resp = HttpResponse::Ok();
147 resp.status(status_code);
148 resp.insert_header(header::ContentType(self.content_type));
149 if let Some(etag) = self.etag {
150 resp.insert_header(header::ETag(etag));
151 }
152 resp.body(self.content)
153 }
154}
155
156impl Responder for EmbedFile {
157 type Body = BoxBody;
158
159 fn respond_to(self, req: &HttpRequest) -> HttpResponse {
160 self.into_response(req)
161 }
162}