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;
pub async fn embedded<E: RustEmbed>(path: web::Path<PathBuf>) -> Result<EmbedFile, Error> {
Ok(EmbedFile::open::<E, _>(path.as_ref())?)
}
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>>;
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
}
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)
}
}