ferron/util/
generate_directory_listing.rs

1use 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  // Return path
21  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  // Create a vector containing entries, then sort them by file name.
36  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      // Don't add files nor directories with "." at the beginning of their names
46      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}