211 lines
4.4 KiB
Rust
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::FileServer,
|
|
) -> 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", ¶ms).map_err (anyhow::Error::from)?;
|
|
|
|
Ok (serve (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,
|
|
}).map_err (anyhow::Error::from)?;
|
|
|
|
Ok (serve (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 (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);
|
|
}
|
|
}
|
|
}
|