ptth/crates/ptth_server/src/file_server/html.rs

211 lines
4.4 KiB
Rust

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 <DirEntry>,
}
#[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 <Response, FileServerError>
{
#[derive (Serialize)]
struct RootHtml <'a> {
metrics_startup: &'a metrics::Startup,
metrics_interval: &'a Option <metrics::Interval>,
}
let params = RootHtml {
metrics_startup: &state.metrics_startup,
metrics_interval: &**state.metrics_interval.load (),
};
let s = state.handlebars.render ("file_server_root", &params)?;
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 <Response, FileServerError>
{
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);
}
}
}