use actix_web::{fs, HttpRequest, HttpResponse, Result}; use bytesize::ByteSize; use htmlescape::encode_minimal as escape_html_entity; use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET}; use std::cmp::Ordering; use std::fmt::Write as FmtWrite; use std::io; use std::path::Path; use std::str::FromStr; #[derive(Clone, Copy, Debug)] pub enum SortingMethods { Natural, Alpha, DirsFirst, } #[derive(PartialEq)] enum EntryType { Directory, File, } impl PartialOrd for EntryType { fn partial_cmp(&self, other: &EntryType) -> Option { match (self, other) { (EntryType::Directory, EntryType::File) => Some(Ordering::Less), (EntryType::File, EntryType::Directory) => Some(Ordering::Greater), _ => Some(Ordering::Equal), } } } struct Entry { name: String, entry_type: EntryType, link: String, size: Option, } impl Entry { fn new( name: String, entry_type: EntryType, link: String, size: Option, ) -> Self { Entry { name, entry_type, link, size, } } } impl FromStr for SortingMethods { type Err = (); fn from_str(s: &str) -> Result { match s { "natural" => Ok(SortingMethods::Natural), "alpha" => Ok(SortingMethods::Alpha), "dirsfirst" => Ok(SortingMethods::DirsFirst), _ => Err(()), } } } pub fn file_handler(req: &HttpRequest) -> Result { let path = &req.state().path; Ok(fs::NamedFile::open(path)?) } // ↓ Adapted from https://docs.rs/actix-web/0.7.13/src/actix_web/fs.rs.html#564 pub fn directory_listing( dir: &fs::Directory, req: &HttpRequest, skip_symlinks: bool, random_route: Option, sort_method: SortingMethods, reverse_sort: bool, ) -> Result { let index_of = format!("Index of {}", req.path()); let mut body = String::new(); let base = Path::new(req.path()); let random_route = format!("/{}", random_route.unwrap_or_default()); if let Some(parent) = base.parent() { if req.path() != random_route { let _ = write!( body, "..", parent.display() ); } } let mut entries: Vec = Vec::new(); 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) { Ok(p) => base.join(p), Err(_) => continue, }; // show file url as relative to static path let file_url = utf8_percent_encode(&p.to_string_lossy(), DEFAULT_ENCODE_SET).to_string(); // " -- " & -- & ' -- ' < -- < > -- > let file_name = escape_html_entity(&entry.file_name().to_string_lossy()); // if file is a directory, add '/' to the end of the name if let Ok(metadata) = entry.metadata() { if skip_symlinks && metadata.file_type().is_symlink() { continue; } if metadata.is_dir() { entries.push(Entry::new(file_name, EntryType::Directory, file_url, None)); } else { entries.push(Entry::new( file_name, EntryType::File, file_url, Some(ByteSize::b(metadata.len())), )); } } else { continue; } } } match sort_method { SortingMethods::Natural => entries .sort_by(|e1, e2| alphanumeric_sort::compare_str(e1.name.clone(), e2.name.clone())), SortingMethods::Alpha => { entries.sort_by(|e1, e2| e1.entry_type.partial_cmp(&e2.entry_type).unwrap()); entries.sort_by_key(|e| e.name.clone()) } SortingMethods::DirsFirst => { entries.sort_by_key(|e| e.name.clone()); entries.sort_by(|e1, e2| e1.entry_type.partial_cmp(&e2.entry_type).unwrap()); } }; if reverse_sort { entries.reverse(); } for entry in entries { match entry.entry_type { EntryType::Directory => { let _ = write!( body, "{}/", entry.link, entry.name ); } EntryType::File => { let _ = write!( body, "{}{}", entry.link, entry.name, entry.size.unwrap() ); } } } let html = format!( "\ \ {}\ \ \

{}

\ \ \ \ {}\
NameSize
\n", index_of, index_of, body ); Ok(HttpResponse::Ok() .content_type("text/html; charset=utf-8") .body(html)) }