1use std::{
2 fmt::Write,
3 fs::DirEntry,
4 io,
5 path::{Path, PathBuf},
6};
7
8use actix_web::{dev::ServiceResponse, HttpRequest, HttpResponse};
9use percent_encoding::{utf8_percent_encode, CONTROLS};
10use v_htmlescape::escape as escape_html_entity;
11
12#[derive(Debug)]
14pub struct Directory {
15 pub base: PathBuf,
17
18 pub path: PathBuf,
20}
21
22impl Directory {
23 pub fn new(base: PathBuf, path: PathBuf) -> Directory {
25 Directory { base, path }
26 }
27
28 pub fn is_visible(&self, entry: &io::Result<DirEntry>) -> bool {
30 if let Ok(ref entry) = *entry {
31 if let Some(name) = entry.file_name().to_str() {
32 if name.starts_with('.') {
33 return false;
34 }
35 }
36 if let Ok(ref md) = entry.metadata() {
37 let ft = md.file_type();
38 return ft.is_dir() || ft.is_file() || ft.is_symlink();
39 }
40 }
41 false
42 }
43}
44
45pub(crate) type DirectoryRenderer =
46 dyn Fn(&Directory, &HttpRequest) -> Result<ServiceResponse, io::Error>;
47
48macro_rules! encode_file_url {
50 ($path:ident) => {
51 utf8_percent_encode(&$path, CONTROLS)
52 };
53}
54
55macro_rules! encode_file_name {
66 ($entry:ident) => {
67 escape_html_entity(&$entry.file_name().to_string_lossy())
68 };
69}
70
71pub(crate) fn directory_listing(
72 dir: &Directory,
73 req: &HttpRequest,
74) -> Result<ServiceResponse, io::Error> {
75 let index_of = format!("Index of {}", req.path());
76 let mut body = String::new();
77 let base = Path::new(req.path());
78
79 for entry in dir.path.read_dir()? {
80 if dir.is_visible(&entry) {
81 let entry = entry.unwrap();
82 let p = match entry.path().strip_prefix(&dir.path) {
83 Ok(p) if cfg!(windows) => base.join(p).to_string_lossy().replace('\\', "/"),
84 Ok(p) => base.join(p).to_string_lossy().into_owned(),
85 Err(_) => continue,
86 };
87
88 if let Ok(metadata) = entry.metadata() {
90 if metadata.is_dir() {
91 let _ = write!(
92 body,
93 "<li><a href=\"{}\">{}/</a></li>",
94 encode_file_url!(p),
95 encode_file_name!(entry),
96 );
97 } else {
98 let _ = write!(
99 body,
100 "<li><a href=\"{}\">{}</a></li>",
101 encode_file_url!(p),
102 encode_file_name!(entry),
103 );
104 }
105 } else {
106 continue;
107 }
108 }
109 }
110
111 let html = format!(
112 "<html>\
113 <head><title>{}</title></head>\
114 <body><h1>{}</h1>\
115 <ul>\
116 {}\
117 </ul></body>\n</html>",
118 index_of, index_of, body
119 );
120 Ok(ServiceResponse::new(
121 req.clone(),
122 HttpResponse::Ok()
123 .content_type("text/html; charset=utf-8")
124 .body(html),
125 ))
126}