1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
use actix_web::{
    body::BoxBody,
    http::{header, StatusCode},
    web, Error, HttpMessage, HttpRequest, HttpResponse, Responder,
};
use rust_embed::RustEmbed;
use std::cell::RefCell;
use std::collections::{BTreeMap, HashMap};
use std::io;
use std::path::{Path, PathBuf};

#[derive(RustEmbed)]
#[folder = "src/empty/"]
pub struct EmptyDir;

/// endpoint for static files
pub async fn embedded<E: RustEmbed>(path: web::Path<PathBuf>) -> Result<EmbedFile, Error> {
    Ok(EmbedFile::open::<E, _>(path.as_ref())?)
}

/// endpoint for static files with index.html
pub async fn embedded_index<E: RustEmbed>(path: web::Path<PathBuf>) -> Result<EmbedFile, Error> {
    let filename = if path.as_ref() == &PathBuf::from("") {
        PathBuf::from("index.html")
    } else {
        path.to_path_buf()
    };
    Ok(EmbedFile::open::<E, _>(filename)?)
}

type EtagMap = HashMap<&'static str, BTreeMap<String, u64>>;

// ETags of resource in RustEmbed classes should never be changed since resources be embeded into the binary.
// To avoid repeatable calculate ETag, make a Pool to store these constant etag value.
// Use thread_local to avoid lock acquires between threads.
thread_local! {
    static ETAG: RefCell<EtagMap> = init();
}

fn init() -> RefCell<EtagMap> {
    RefCell::new(EtagMap::new())
}

fn get_etag<E>(filename: &str) -> Option<u64>
where
    E: RustEmbed,
{
    let filename = filename.to_string();
    let typename = std::any::type_name::<E>();
    ETAG.with(|m| {
        if let Some(map) = m.borrow().get(typename) {
            return map.get(&filename).copied();
        }
        let map = init_etag::<E>();
        let r = map.get(&filename).copied();
        m.borrow_mut().insert(typename, map);
        r
    })
}

fn init_etag<E>() -> BTreeMap<String, u64>
where
    E: RustEmbed,
{
    let mut map = BTreeMap::new();
    for file in E::iter() {
        let file = file.as_ref();
        let etag = match E::get(file).map(|c| fxhash::hash64(&c.data)) {
            Some(etag) => etag,
            None => continue,
        };
        map.insert(file.into(), etag);
    }
    map
}

/// Returns true if `req` doesn't have an `If-None-Match` header matching `req`.
fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
    match req.get_header::<header::IfNoneMatch>() {
        Some(header::IfNoneMatch::Any) => false,

        Some(header::IfNoneMatch::Items(ref items)) => {
            if let Some(some_etag) = etag {
                for item in items {
                    if item.weak_eq(some_etag) {
                        return false;
                    }
                }
            }

            true
        }

        None => true,
    }
}

fn io_not_found<S>(info: S) -> io::Error
where
    S: AsRef<str>,
{
    io::Error::new(io::ErrorKind::NotFound, info.as_ref())
}

pub struct EmbedFile {
    content: Vec<u8>,
    content_type: mime::Mime,
    etag: Option<header::EntityTag>,
}

impl EmbedFile {
    pub fn open<E, P>(path: P) -> io::Result<EmbedFile>
    where
        E: RustEmbed,
        P: AsRef<Path>,
    {
        let mut path = path.as_ref();
        while let Ok(new_path) = path.strip_prefix(".") {
            path = new_path;
        }
        Self::open_impl::<E>(path).ok_or(io_not_found("File not found"))
    }

    fn open_impl<E>(path: &Path) -> Option<EmbedFile>
    where
        E: RustEmbed,
    {
        let content_type = mime_guess::from_path(path).first_or_octet_stream();
        let filename = path.to_str()?;
        let etag = get_etag::<E>(filename);
        let r = EmbedFile {
            content: E::get(filename)?.data.to_vec(),
            content_type,
            etag: etag.map(|etag| header::EntityTag::new_strong(format!("{:x}", etag))),
        };
        Some(r)
    }

    fn into_response(self, req: &HttpRequest) -> HttpResponse {
        let status_code = if !none_match(self.etag.as_ref(), req) {
            StatusCode::NOT_MODIFIED
        } else {
            StatusCode::OK
        };

        let mut resp = HttpResponse::Ok();
        resp.status(status_code);
        resp.insert_header(header::ContentType(self.content_type));
        if let Some(etag) = self.etag {
            resp.insert_header(header::ETag(etag));
        }
        resp.body(self.content)
    }
}

impl Responder for EmbedFile {
    type Body = BoxBody;

    fn respond_to(self, req: &HttpRequest) -> HttpResponse {
        self.into_response(req)
    }
}