♻️ refactor: extract html.rs

main
_ 2020-12-20 18:38:39 +00:00
parent 4bd38180d0
commit 066c95dc07
4 changed files with 204 additions and 190 deletions

View File

@ -0,0 +1,196 @@
use std::borrow::Cow;
use handlebars::Handlebars;
use serde::Serialize;
use tracing::instrument;
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 (
handlebars: &Handlebars <'static>,
instance_metrics: &metrics::Startup
) -> Result <Response, FileServerError>
{
let s = handlebars.render ("file_server_root", &instance_metrics)?;
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: tokio::fs::ReadDir
) -> 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: tokio::fs::DirEntry) -> 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);
}
}
}

View File

@ -4,7 +4,6 @@
#![allow (clippy::enum_glob_use)] #![allow (clippy::enum_glob_use)]
use std::{ use std::{
borrow::Cow,
cmp::min, cmp::min,
collections::HashMap, collections::HashMap,
convert::{Infallible, TryFrom}, convert::{Infallible, TryFrom},
@ -42,20 +41,13 @@ use ptth_core::{
pub mod errors; pub mod errors;
pub mod metrics; pub mod metrics;
mod html;
mod internal; mod internal;
mod markdown; mod markdown;
mod range; mod range;
use errors::FileServerError; use errors::FileServerError;
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}";
}
#[derive (Default)] #[derive (Default)]
pub struct Config { pub struct Config {
pub file_server_root: Option <PathBuf>, pub file_server_root: Option <PathBuf>,
@ -68,122 +60,16 @@ pub struct State {
pub hidden_path: Option <PathBuf>, pub hidden_path: Option <PathBuf>,
} }
#[derive (Serialize)]
struct DirEntryJson {
name: String,
size: u64,
is_dir: bool,
}
#[derive (Serialize)] #[derive (Serialize)]
struct DirJson { struct DirJson {
entries: Vec <DirEntryJson>, entries: Vec <DirEntryJson>,
} }
#[derive (Serialize)] #[derive (Serialize)]
struct DirEntryHtml { struct DirEntryJson {
icon: &'static str, name: String,
trailing_slash: &'static str, size: u64,
is_dir: bool,
// 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,
}
#[derive (Serialize)]
struct DirHtml <'a> {
#[serde (flatten)]
instance_metrics: &'a metrics::Startup,
path: Cow <'a, str>,
entries: Vec <DirEntryHtml>,
}
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
}
}
async fn read_dir_entry_html (entry: DirEntry) -> DirEntryHtml
{
use percent_encoding::{
CONTROLS,
utf8_percent_encode,
};
let file_name = match entry.file_name ().into_string () {
Ok (x) => x,
Err (_) => return DirEntryHtml {
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 DirEntryHtml {
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 ();
DirEntryHtml {
icon,
trailing_slash: &trailing_slash,
file_name,
encoded_file_name,
size,
error: false,
}
} }
async fn read_dir_entry_json (entry: DirEntry) -> Option <DirEntryJson> async fn read_dir_entry_json (entry: DirEntry) -> Option <DirEntryJson>
@ -200,25 +86,6 @@ async fn read_dir_entry_json (entry: DirEntry) -> Option <DirEntryJson>
}) })
} }
async fn serve_root (
handlebars: &Handlebars <'static>,
instance_metrics: &metrics::Startup
) -> Result <Response, FileServerError>
{
let s = handlebars.render ("file_server_root", &instance_metrics)?;
Ok (serve_html (s))
}
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
}
async fn serve_dir_json ( async fn serve_dir_json (
mut dir: ReadDir mut dir: ReadDir
) -> Result <Response, FileServerError> ) -> Result <Response, FileServerError>
@ -244,31 +111,6 @@ async fn serve_dir_json (
Ok (response) Ok (response)
} }
#[instrument (level = "debug", skip (handlebars, instance_metrics, dir))]
async fn serve_dir_html (
handlebars: &Handlebars <'static>,
instance_metrics: &metrics::Startup,
path: Cow <'_, str>,
mut dir: ReadDir
) -> Result <Response, FileServerError>
{
let mut entries = vec! [];
while let Ok (Some (entry)) = dir.next_entry ().await {
entries.push (read_dir_entry_html (entry).await);
}
entries.sort_unstable_by (|a, b| a.file_name.cmp (&b.file_name));
let s = handlebars.render ("file_server_dir", &DirHtml {
path,
entries,
instance_metrics,
})?;
Ok (serve_html (s))
}
#[instrument (level = "debug", skip (f))] #[instrument (level = "debug", skip (f))]
async fn serve_file ( async fn serve_file (
mut f: File, mut f: File,
@ -419,14 +261,14 @@ pub async fn serve_all (
}, },
InvalidQuery => serve_error (StatusCode::BadRequest, "Query is invalid for this object\n"), InvalidQuery => serve_error (StatusCode::BadRequest, "Query is invalid for this object\n"),
Root => serve_root (handlebars, instance_metrics).await?, Root => html::serve_root (handlebars, instance_metrics).await?,
ServeDir (internal::ServeDirParams { ServeDir (internal::ServeDirParams {
path, path,
dir, dir,
format format
}) => match format { }) => match format {
OutputFormat::Json => serve_dir_json (dir.into_inner ()).await?, OutputFormat::Json => serve_dir_json (dir.into_inner ()).await?,
OutputFormat::Html => serve_dir_html (handlebars, instance_metrics, path.to_string_lossy (), dir.into_inner ()).await?, OutputFormat::Html => html::serve_dir (handlebars, instance_metrics, path.to_string_lossy (), dir.into_inner ()).await?,
}, },
ServeFile (internal::ServeFileParams { ServeFile (internal::ServeFileParams {
file, file,
@ -443,7 +285,7 @@ pub async fn serve_all (
serve_error (code, e.to_string ()) serve_error (code, e.to_string ())
}, },
MarkdownPreview (s) => serve_html (s), MarkdownPreview (s) => html::serve_html (s),
}) })
} }

View File

@ -9,29 +9,6 @@ use std::{
use maplit::*; use maplit::*;
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
#[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);
}
}
#[test] #[test]
fn pretty_print_bytes () { fn pretty_print_bytes () {
for (input_after, expected_before, expected_after) in vec! [ for (input_after, expected_before, expected_after) in vec! [

View File

@ -14,7 +14,6 @@ use std::{
}; };
use futures::FutureExt; use futures::FutureExt;
use handlebars::Handlebars;
use reqwest::Client; use reqwest::Client;
use serde::Deserialize; use serde::Deserialize;
use tokio::{ use tokio::{