use anyhow::{Context, Result};
use chrono::prelude::*;
use chrono::{DateTime, Local};
use handlebars::Handlebars;
use http::response::Builder as HttpResponseBuilder;
use http::{Method, StatusCode};
use hyper::{Body, Request, Response};
use serde::Serialize;
use std::cmp::{Ord, Ordering};
use std::fs::read_dir;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::SystemTime;
use crate::addon::static_file::http::{make_http_file_response, CacheControlDirective};
use crate::addon::static_file::{Entry, ScopedFileSystem};
use crate::server::middleware::Handler;
pub fn make_file_explorer_handler(file_explorer: Arc<FileExplorer>) -> Handler {
Box::new(move |request: Arc<Request<Body>>| {
let file_explorer = Arc::clone(&file_explorer);
let req_path = request.uri().to_string();
Box::pin(async move {
if request.method() == Method::GET {
return file_explorer
.resolve(req_path)
.await
.map_err(|e| {
HttpResponseBuilder::new()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from(e.to_string()))
.expect("Unable to build response")
})
.unwrap();
}
HttpResponseBuilder::new()
.status(StatusCode::METHOD_NOT_ALLOWED)
.body(Body::empty())
.expect("Unable to build response")
})
})
}
const EXPLORER_TEMPLATE: &str = "explorer";
const BYTE_SIZE_UNIT: [&str; 9] = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
#[derive(Debug, Eq, Serialize)]
struct DirectoryEntry {
display_name: String,
is_dir: bool,
size: String,
entry_path: String,
created_at: String,
updated_at: String,
}
impl Ord for DirectoryEntry {
fn cmp(&self, other: &Self) -> Ordering {
if self.is_dir && other.is_dir {
return self.display_name.cmp(&other.display_name);
}
if self.is_dir && !other.is_dir {
return Ordering::Less;
}
Ordering::Greater
}
}
impl PartialOrd for DirectoryEntry {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
if self.is_dir && other.is_dir {
return Some(self.display_name.cmp(&other.display_name));
}
if self.is_dir && !other.is_dir {
return Some(Ordering::Less);
}
Some(Ordering::Greater)
}
}
impl PartialEq for DirectoryEntry {
fn eq(&self, other: &Self) -> bool {
if self.is_dir && other.is_dir {
return self.display_name == other.display_name;
}
self.display_name == other.display_name
}
}
#[derive(Debug, Serialize)]
struct DirectoryIndex {
entries: Vec<DirectoryEntry>,
}
#[derive(Clone)]
pub struct FileExplorer {
root_dir: PathBuf,
cache_headers: Option<u32>,
handlebars: Arc<Handlebars<'static>>,
scoped_file_system: ScopedFileSystem,
}
impl<'a> FileExplorer {
pub fn new(root_dir: PathBuf) -> Self {
let handlebars = FileExplorer::make_handlebars_engine();
let scoped_file_system = ScopedFileSystem::new(root_dir.clone()).unwrap();
FileExplorer {
root_dir,
cache_headers: None,
handlebars,
scoped_file_system,
}
}
fn make_handlebars_engine() -> Arc<Handlebars<'a>> {
let mut handlebars = Handlebars::new();
let template = std::include_bytes!("../template/explorer.hbs");
let template = std::str::from_utf8(template).unwrap();
handlebars
.register_template_string(EXPLORER_TEMPLATE, template)
.unwrap();
Arc::new(handlebars)
}
pub async fn resolve(&self, req_path: String) -> Result<Response<Body>> {
let path = PathBuf::from(req_path);
if let Ok(entry) = self.scoped_file_system.resolve(path).await {
return match entry {
Entry::Directory(dir) => self.render_directory_index(dir.path()).await,
Entry::File(file) => {
make_http_file_response(file, CacheControlDirective::MaxAge(2500)).await
}
};
}
Ok(HttpResponseBuilder::new()
.status(StatusCode::BAD_REQUEST)
.body(Body::empty())
.expect("Failed to build response"))
}
async fn render_directory_index(&self, path: PathBuf) -> Result<Response<Body>> {
let directory_index = FileExplorer::index_directory(self.root_dir.clone(), path)?;
let html = self
.handlebars
.render(EXPLORER_TEMPLATE, &directory_index)
.unwrap();
let body = Body::from(html);
Ok(HttpResponseBuilder::new()
.header(http::header::CONTENT_TYPE, "text/html")
.status(StatusCode::OK)
.body(body)
.expect("Failed to build response"))
}
fn index_directory(root_dir: PathBuf, path: PathBuf) -> Result<DirectoryIndex> {
let entries = read_dir(path).context("Unable to read directory")?;
let mut directory_entries: Vec<DirectoryEntry> = Vec::new();
for entry in entries.into_iter() {
let entry = entry.context("Unable to read entry")?;
let metadata = entry.metadata()?;
let created_at = if let Ok(time) = metadata.created() {
FileExplorer::format_system_date(time)
} else {
String::default()
};
let updated_at = if let Ok(time) = metadata.modified() {
FileExplorer::format_system_date(time)
} else {
String::default()
};
directory_entries.push(DirectoryEntry {
display_name: entry
.file_name()
.to_str()
.context("Unable to gather file name into a String")?
.to_string(),
is_dir: metadata.is_dir(),
size: FileExplorer::format_bytes(metadata.len() as f64),
entry_path: FileExplorer::make_dir_entry_link(&root_dir, &entry.path()),
created_at,
updated_at,
});
}
directory_entries.sort();
Ok(DirectoryIndex {
entries: directory_entries,
})
}
fn make_dir_entry_link(root_dir: &Path, entry_path: &Path) -> String {
let root_dir = root_dir.to_str().unwrap();
let entry_path = entry_path.to_str().unwrap();
entry_path[root_dir.len()..].to_string()
}
fn format_bytes(bytes: f64) -> String {
if bytes == 0. {
return String::from("0 Bytes");
}
let i = (bytes.log10() / 1024_f64.log10()).floor();
let value = bytes / 1024_f64.powf(i);
format!("{:.2} {}", value, BYTE_SIZE_UNIT[i as usize])
}
fn format_system_date(system_time: SystemTime) -> String {
let datetime: DateTime<Local> = DateTime::from(system_time);
format!(
"{}/{:0>2}/{:0>2} {:0>2}:{:0>2}:{:0>2}",
datetime.year(),
datetime.month(),
datetime.day(),
datetime.hour(),
datetime.minute(),
datetime.second()
)
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use std::vec;
use super::*;
#[test]
fn formats_bytes() {
let byte_sizes = vec![1024., 1048576., 1073741824., 1099511627776.];
let expect = vec![
String::from("1.00 KB"),
String::from("1.00 MB"),
String::from("1.00 GB"),
String::from("1.00 TB"),
];
for (idx, size) in byte_sizes.into_iter().enumerate() {
assert_eq!(FileExplorer::format_bytes(size), expect[idx]);
}
}
#[test]
fn makes_dir_entry_link() {
let root_dir = PathBuf::from_str("/Users/bob/sources/http-server").unwrap();
let entry_path =
PathBuf::from_str("/Users/bob/sources/http-server/src/server/service/file_explorer.rs")
.unwrap();
assert_eq!(
"/src/server/service/file_explorer.rs",
FileExplorer::make_dir_entry_link(&root_dir, &entry_path)
);
}
}