use std::borrow::Cow; use handlebars::Handlebars; use serde::Serialize; use tracing::instrument; use tokio::fs::{ DirEntry as FsDirEntry, ReadDir as FsReadDir, }; use super::{ FileServerError, Response, metrics, pretty_print_bytes, }; #[derive (Serialize)] struct Dir <'a> { #[serde (flatten)] instance_metrics: &'a metrics::Startup, path: Cow <'a, str>, entries: Vec , } #[derive (Serialize)] struct DirEntry { icon: &'static str, trailing_slash: &'static str, // Unfortunately file_name will allocate as long as some platforms // (Windows!) aren't UTF-8. Cause I don't want to write separate code // for such a small problem. file_name: String, // This could be a Cow with file_name if no encoding was done but // it's simpler to allocate. encoded_file_name: String, size: Cow <'static, str>, error: bool, } pub async fn serve_root ( state: &super::State, ) -> Result { #[derive (Serialize)] struct RootHtml <'a> { metrics_startup: &'a metrics::Startup, metrics_interval: &'a Option , } let params = RootHtml { metrics_startup: &state.metrics_startup, metrics_interval: &**state.metrics_interval.load (), }; let s = state.handlebars.render ("file_server_root", ¶ms)?; Ok (serve_html (s)) } #[instrument (level = "debug", skip (handlebars, instance_metrics, dir))] pub async fn serve_dir ( handlebars: &Handlebars <'static>, instance_metrics: &metrics::Startup, path: Cow <'_, str>, mut dir: FsReadDir ) -> Result { let mut entries = vec! []; while let Ok (Some (entry)) = dir.next_entry ().await { entries.push (read_dir_entry (entry).await); } entries.sort_unstable_by (|a, b| a.file_name.cmp (&b.file_name)); let s = handlebars.render ("file_server_dir", &Dir { path, entries, instance_metrics, })?; Ok (serve_html (s)) } async fn read_dir_entry (entry: FsDirEntry) -> DirEntry { use percent_encoding::{ CONTROLS, utf8_percent_encode, }; let file_name = match entry.file_name ().into_string () { Ok (x) => x, Err (_) => return DirEntry { icon: emoji::ERROR, trailing_slash: "", file_name: "File / directory name is not UTF-8".into (), encoded_file_name: "".into (), size: "".into (), error: true, }, }; let metadata = match entry.metadata ().await { Ok (x) => x, Err (_) => return DirEntry { icon: emoji::ERROR, trailing_slash: "", file_name: "Could not fetch metadata".into (), encoded_file_name: "".into (), size: "".into (), error: true, }, }; let (trailing_slash, icon, size) = { let t = metadata.file_type (); if t.is_dir () { ("/", emoji::FOLDER, "".into ()) } else { ("", get_icon (&file_name), pretty_print_bytes (metadata.len ()).into ()) } }; let encoded_file_name = utf8_percent_encode (&file_name, CONTROLS).to_string (); DirEntry { icon, trailing_slash: &trailing_slash, file_name, encoded_file_name, size, error: false, } } pub fn serve_html (s: String) -> Response { let mut resp = Response::default (); resp .header ("content-type".to_string (), "text/html; charset=UTF-8".to_string ().into_bytes ()) .body_bytes (s.into_bytes ()) ; resp } fn get_icon (file_name: &str) -> &'static str { if file_name.ends_with (".mp4") || file_name.ends_with (".avi") || file_name.ends_with (".mkv") || file_name.ends_with (".webm") { emoji::VIDEO } else if file_name.ends_with (".jpg") || file_name.ends_with (".jpeg") || file_name.ends_with (".png") || file_name.ends_with (".bmp") { emoji::PICTURE } else { emoji::FILE } } mod emoji { pub const VIDEO: &str = "\u{1f39e}\u{fe0f}"; pub const PICTURE: &str = "\u{1f4f7}"; pub const FILE: &str = "\u{1f4c4}"; pub const FOLDER: &str = "\u{1f4c1}"; pub const ERROR: &str = "\u{26a0}\u{fe0f}"; } #[cfg (test)] mod tests { #[test] fn icons () { let video = "🎞️"; let picture = "📷"; let file = "📄"; for (input, expected) in vec! [ ("copying_is_not_theft.mp4", video), ("copying_is_not_theft.avi", video), ("copying_is_not_theft.mkv", video), ("copying_is_not_theft.webm", video), ("lolcats.jpg", picture), ("lolcats.jpeg", picture), ("lolcats.png", picture), ("lolcats.bmp", picture), ("ptth.log", file), ("README.md", file), ("todo.txt", file), ].into_iter () { assert_eq! (super::get_icon (input), expected); } } }