bbox_core/
static_files.rs

1use 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
16/// endpoint for static files
17pub async fn embedded<E: RustEmbed>(path: web::Path<PathBuf>) -> Result<EmbedFile, Error> {
18    Ok(EmbedFile::open::<E, _>(path.as_ref())?)
19}
20
21/// endpoint for static files with index.html
22pub 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
33// ETags of resource in RustEmbed classes should never be changed since resources be embeded into the binary.
34// To avoid repeatable calculate ETag, make a Pool to store these constant etag value.
35// Use thread_local to avoid lock acquires between threads.
36thread_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
77/// Returns true if `req` doesn't have an `If-None-Match` header matching `req`.
78fn 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}