2020-09-21 00:18:25 +02:00
|
|
|
use std::{fmt::Write, fs::DirEntry, io, path::Path, path::PathBuf};
|
|
|
|
|
|
|
|
use actix_web::{dev::ServiceResponse, HttpRequest, HttpResponse};
|
2021-02-07 05:50:23 +01:00
|
|
|
use askama_escape::{escape as escape_html_entity, Html};
|
2020-09-21 00:18:25 +02:00
|
|
|
use percent_encoding::{utf8_percent_encode, CONTROLS};
|
|
|
|
|
|
|
|
/// A directory; responds with the generated directory listing.
|
|
|
|
#[derive(Debug)]
|
|
|
|
pub struct Directory {
|
|
|
|
/// Base directory.
|
|
|
|
pub base: PathBuf,
|
|
|
|
|
|
|
|
/// Path of subdirectory to generate listing for.
|
|
|
|
pub path: PathBuf,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Directory {
|
|
|
|
/// Create a new directory
|
|
|
|
pub fn new(base: PathBuf, path: PathBuf) -> Directory {
|
|
|
|
Directory { base, path }
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Is this entry visible from this directory?
|
|
|
|
pub fn is_visible(&self, entry: &io::Result<DirEntry>) -> bool {
|
|
|
|
if let Ok(ref entry) = *entry {
|
|
|
|
if let Some(name) = entry.file_name().to_str() {
|
|
|
|
if name.starts_with('.') {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if let Ok(ref md) = entry.metadata() {
|
|
|
|
let ft = md.file_type();
|
|
|
|
return ft.is_dir() || ft.is_file() || ft.is_symlink();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) type DirectoryRenderer =
|
|
|
|
dyn Fn(&Directory, &HttpRequest) -> Result<ServiceResponse, io::Error>;
|
|
|
|
|
|
|
|
// show file url as relative to static path
|
|
|
|
macro_rules! encode_file_url {
|
|
|
|
($path:ident) => {
|
|
|
|
utf8_percent_encode(&$path, CONTROLS)
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// " -- " & -- & ' -- ' < -- < > -- > / -- /
|
|
|
|
macro_rules! encode_file_name {
|
|
|
|
($entry:ident) => {
|
2021-02-07 05:50:23 +01:00
|
|
|
escape_html_entity(&$entry.file_name().to_string_lossy(), Html)
|
2020-09-21 00:18:25 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn directory_listing(
|
|
|
|
dir: &Directory,
|
|
|
|
req: &HttpRequest,
|
|
|
|
) -> Result<ServiceResponse, io::Error> {
|
|
|
|
let index_of = format!("Index of {}", req.path());
|
|
|
|
let mut body = String::new();
|
|
|
|
let base = Path::new(req.path());
|
|
|
|
|
|
|
|
for entry in dir.path.read_dir()? {
|
|
|
|
if dir.is_visible(&entry) {
|
|
|
|
let entry = entry.unwrap();
|
|
|
|
let p = match entry.path().strip_prefix(&dir.path) {
|
2021-02-12 00:03:17 +01:00
|
|
|
Ok(p) if cfg!(windows) => base.join(p).to_string_lossy().replace("\\", "/"),
|
2020-09-21 00:18:25 +02:00
|
|
|
Ok(p) => base.join(p).to_string_lossy().into_owned(),
|
|
|
|
Err(_) => continue,
|
|
|
|
};
|
|
|
|
|
|
|
|
// if file is a directory, add '/' to the end of the name
|
|
|
|
if let Ok(metadata) = entry.metadata() {
|
|
|
|
if metadata.is_dir() {
|
|
|
|
let _ = write!(
|
|
|
|
body,
|
|
|
|
"<li><a href=\"{}\">{}/</a></li>",
|
|
|
|
encode_file_url!(p),
|
|
|
|
encode_file_name!(entry),
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
let _ = write!(
|
|
|
|
body,
|
|
|
|
"<li><a href=\"{}\">{}</a></li>",
|
|
|
|
encode_file_url!(p),
|
|
|
|
encode_file_name!(entry),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let html = format!(
|
|
|
|
"<html>\
|
|
|
|
<head><title>{}</title></head>\
|
|
|
|
<body><h1>{}</h1>\
|
|
|
|
<ul>\
|
|
|
|
{}\
|
|
|
|
</ul></body>\n</html>",
|
|
|
|
index_of, index_of, body
|
|
|
|
);
|
|
|
|
Ok(ServiceResponse::new(
|
|
|
|
req.clone(),
|
|
|
|
HttpResponse::Ok()
|
|
|
|
.content_type("text/html; charset=utf-8")
|
|
|
|
.body(html),
|
|
|
|
))
|
|
|
|
}
|