ferron/util/
generate_directory_listing.rs1use std::error::Error;
2
3use chrono::{DateTime, Local};
4use tokio::fs::ReadDir;
5
6use crate::ferron_util::anti_xss::anti_xss;
7use crate::ferron_util::sizify::sizify;
8
9pub async fn generate_directory_listing(
10 mut directory: ReadDir,
11 request_path: &str,
12 description: Option<String>,
13) -> Result<String, Box<dyn Error + Send + Sync>> {
14 let mut request_path_without_trailing_slashes = request_path;
15 while request_path_without_trailing_slashes.ends_with("/") {
16 request_path_without_trailing_slashes =
17 &request_path_without_trailing_slashes[..(request_path_without_trailing_slashes.len() - 1)];
18 }
19
20 let mut return_path_vec: Vec<&str> = request_path_without_trailing_slashes.split("/").collect();
22 return_path_vec.pop();
23 return_path_vec.push("");
24 let return_path = &return_path_vec.join("/") as &str;
25
26 let mut table_rows = Vec::new();
27 if !request_path_without_trailing_slashes.is_empty() {
28 table_rows.push(format!(
29 "<tr><td><a href=\"{}\">Return</a></td><td></td><td></td></tr>",
30 anti_xss(return_path)
31 ));
32 }
33 let min_table_rows_length = table_rows.len();
34
35 let mut entries = Vec::new();
37 while let Some(entry) = directory.next_entry().await? {
38 entries.push(entry);
39 }
40 entries.sort_by_cached_key(|entry| entry.file_name().to_string_lossy().to_string());
41
42 for entry in entries.iter() {
43 let filename = entry.file_name().to_string_lossy().to_string();
44 if filename.starts_with('.') {
45 continue;
47 }
48 match entry.metadata().await {
49 Ok(metadata) => {
50 let filename_link = format!(
51 "<a href=\"{}/{}{}\">{}</a>",
52 request_path_without_trailing_slashes,
53 anti_xss(urlencoding::encode(&filename).as_ref()),
54 match metadata.is_dir() {
55 true => "/",
56 false => "",
57 },
58 anti_xss(&filename)
59 );
60
61 let row = format!(
62 "<tr><td>{}</td><td>{}</td><td>{}</td></tr>",
63 filename_link,
64 match metadata.is_file() {
65 true => anti_xss(&sizify(metadata.len(), false)),
66 false => "-".to_string(),
67 },
68 anti_xss(
69 &(match metadata.modified() {
70 Ok(mtime) => {
71 let datetime: DateTime<Local> = mtime.into();
72 datetime.format("%a %b %d %Y").to_string()
73 }
74 Err(_) => "-".to_string(),
75 })
76 )
77 );
78 table_rows.push(row);
79 }
80 Err(_) => {
81 let filename_link = format!(
82 "<a href=\"{}{}{}\">{}</a>",
83 "{}{}",
84 request_path_without_trailing_slashes,
85 anti_xss(urlencoding::encode(&filename).as_ref()),
86 anti_xss(&filename)
87 );
88 let row = format!("<tr><td>{}</td><td>-</td><td>-</td></tr>", filename_link);
89 table_rows.push(row);
90 }
91 };
92 }
93
94 if table_rows.len() < min_table_rows_length {
95 table_rows.push("<tr><td>No files found</td><td></td><td></td></tr>".to_string());
96 }
97
98 Ok(format!(
99 "<!DOCTYPE html>
100<html lang=\"en\">
101<head>
102 <meta charset=\"UTF-8\">
103 <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">
104 <title>Directory: {}</title>
105</head>
106<body>
107 <h1>Directory: {}</h1>
108 <table>
109 <tr><th>Filename</th><th>Size</th><th>Date</th></tr>
110 {}
111 {}
112 </table>
113</body>
114</html>",
115 anti_xss(request_path),
116 anti_xss(request_path),
117 table_rows.join(""),
118 match description {
119 Some(description) => format!(
120 "<hr>{}",
121 anti_xss(&description)
122 .replace("\r\n", "\n")
123 .replace("\r", "\n")
124 .replace("\n", "<br>")
125 ),
126 None => "".to_string(),
127 }
128 ))
129}