use crate::Response;
use std::{
fs::File,
hash::{DefaultHasher, Hasher},
path::Path,
time::SystemTime,
};
#[cfg_attr(docsrs, doc(cfg(feature = "assets")))]
pub fn get_asset(
path: &str,
etag: Option<&[u8]>,
root: impl AsRef<Path>,
) -> impl Future<Output = Response> {
let root = root.as_ref().to_path_buf();
let path = path.trim_start_matches('/').to_string();
let etag = etag.map(|e| e.to_vec());
blocking::unblock(move || {
let root = match root.canonicalize() {
Ok(p) => p,
Err(_) => return Response::internal_server_error(),
};
let path = match root.join(&path).canonicalize() {
Ok(p) => p,
Err(_) => return Response::not_found(),
};
if !path.starts_with(&root) || !path.is_file() {
return Response::not_found();
}
let metadata = match path.metadata() {
Ok(m) => m,
Err(_) => return Response::internal_server_error(),
};
let tag = make_etag(&metadata);
let ext = get_mime_type(&path);
if let Some(req_etag) = etag
&& let Some(t) = &tag
&& req_etag == t.as_bytes()
{
return Response::not_modified(ext).with_header("ETag", tag.unwrap());
}
if metadata.len() < 16 * 1024
&& let Ok(data) = std::fs::read(&path)
{
let mut resp = Response::new_with_body(data, ext);
if let Some(tag) = tag {
resp = resp.with_header("ETag", tag);
}
return resp;
}
let file = match File::open(path) {
Ok(f) => f,
Err(_) => return Response::internal_server_error(),
};
let mut response = Response::new_with_body(file, ext);
if let Some(tag) = tag {
response = response.with_header("ETag", tag);
}
response
})
}
fn make_etag(metadata: &std::fs::Metadata) -> Option<String> {
let modified = metadata.modified().ok()?;
let modified = match modified.duration_since(SystemTime::UNIX_EPOCH) {
Ok(d) => d,
Err(e) => e.duration(),
};
let mut hasher = DefaultHasher::new();
hasher.write_u64(modified.as_secs());
hasher.write_u32(modified.subsec_nanos());
Some(format!("\"{:x}\"", hasher.finish()))
}
#[cfg_attr(docsrs, doc(cfg(feature = "assets")))]
pub fn get_mime_type<P>(path: &P) -> Option<&'static str>
where
P: AsRef<Path> + ?Sized,
{
let ext = path.as_ref().extension()?.to_str()?;
let ext_lower = ext.to_lowercase();
match ext_lower.as_str() {
"txt" => Some("text/plain"),
"html" | "htm" => Some("text/html"),
"css" => Some("text/css"),
"js" | "mjs" => Some("text/javascript"),
"csv" => Some("text/csv"),
"xml" => Some("text/xml"),
"md" | "markdown" => Some("text/markdown"),
"rtf" => Some("application/rtf"),
"tex" => Some("application/x-tex"),
"jpg" | "jpeg" => Some("image/jpeg"),
"png" => Some("image/png"),
"apng" => Some("image/apng"),
"gif" => Some("image/gif"),
"svg" => Some("image/svg+xml"),
"webp" => Some("image/webp"),
"bmp" => Some("image/bmp"),
"tif" | "tiff" => Some("image/tiff"),
"ico" => Some("image/x-icon"),
"heic" | "heif" => Some("image/heif"),
"avif" => Some("image/avif"),
"mp3" => Some("audio/mpeg"),
"wav" => Some("audio/wav"),
"ogg" | "oga" => Some("audio/ogg"),
"weba" => Some("audio/webm"),
"aac" => Some("audio/aac"),
"flac" => Some("audio/flac"),
"m4a" => Some("audio/mp4"),
"opus" => Some("audio/opus"),
"mp4" | "m4v" => Some("video/mp4"),
"mpeg" | "mpg" => Some("video/mpeg"),
"webm" => Some("video/webm"),
"ogv" => Some("video/ogg"),
"avi" => Some("video/x-msvideo"),
"mov" | "qt" => Some("video/quicktime"),
"mkv" => Some("video/x-matroska"),
"flv" => Some("video/x-flv"),
"wmv" => Some("video/x-ms-wmv"),
"pdf" => Some("application/pdf"),
"zip" => Some("application/zip"),
"rar" => Some("application/x-rar-compressed"),
"json" => Some("application/json"),
"jsonld" => Some("application/ld+json"),
"doc" => Some("application/msword"),
"docx" => Some("application/vnd.openxmlformats-officedocument.wordprocessingml.document"),
"xls" => Some("application/vnd.ms-excel"),
"xlsx" => Some("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
"ppt" => Some("application/vnd.ms-powerpoint"),
"pptx" => Some("application/vnd.openxmlformats-officedocument.presentationml.presentation"),
"7z" => Some("application/x-7z-compressed"),
"tar" => Some("application/x-tar"),
"gz" | "gzip" => Some("application/gzip"),
"bz" | "bz2" => Some("application/x-bzip2"),
"apk" => Some("application/vnd.android.package-archive"),
"jar" => Some("application/java-archive"),
"war" => Some("application/java-archive"),
"exe" => Some("application/x-msdownload"),
"dmg" => Some("application/x-apple-diskimage"),
"deb" => Some("application/x-debian-package"),
"rpm" => Some("application/x-rpm"),
"bin" | "dll" | "so" => Some("application/octet-stream"),
"wasm" => Some("application/wasm"),
"sh" => Some("application/x-sh"),
"sql" => Some("application/sql"),
"yaml" | "yml" => Some("application/x-yaml"),
"toml" => Some("application/toml"),
"woff" => Some("font/woff"),
"woff2" => Some("font/woff2"),
"ttf" => Some("font/ttf"),
"otf" => Some("font/otf"),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_common_extensions() {
assert_eq!(get_mime_type(Path::new("index.html")), Some("text/html"));
assert_eq!(get_mime_type(Path::new("photo.jpg")), Some("image/jpeg"));
assert_eq!(
get_mime_type(Path::new("document.pdf")),
Some("application/pdf")
);
assert_eq!(get_mime_type(Path::new("song.mp3")), Some("audio/mpeg"));
}
#[test]
fn test_case_insensitive() {
assert_eq!(get_mime_type(Path::new("file.HTML")), Some("text/html"));
assert_eq!(get_mime_type(Path::new("image.JpG")), Some("image/jpeg"));
}
#[test]
fn test_unknown_extension() {
assert_eq!(get_mime_type(Path::new("file.unknown")), None);
}
#[test]
fn test_no_extension() {
assert_eq!(get_mime_type(Path::new("README")), None);
}
#[test]
fn test_with_directory() {
assert_eq!(
get_mime_type(Path::new("/path/to/file.json")),
Some("application/json")
);
}
}