diff options
-rw-r--r-- | Cargo.lock | 7 | ||||
-rw-r--r-- | Cargo.toml | 3 | ||||
-rw-r--r-- | README.md | 25 | ||||
-rw-r--r-- | src/main.rs | 159 |
4 files changed, 179 insertions, 15 deletions
@@ -127,6 +127,11 @@ dependencies = [ ] [[package]] +name = "alphanumeric-sort" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] name = "ansi_term" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -688,6 +693,7 @@ version = "0.3.0" dependencies = [ "actix 0.7.9 (registry+https://github.com/rust-lang/crates.io-index)", "actix-web 0.7.18 (registry+https://github.com/rust-lang/crates.io-index)", + "alphanumeric-sort 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "bytesize 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1720,6 +1726,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum actix_derive 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4300e9431455322ae393d43a2ba1ef96b8080573c0fc23b196219efedfb6ba69" "checksum adler32 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7e522997b529f05601e05166c07ed17789691f562762c7f3b987263d2dedee5c" "checksum aho-corasick 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)" = "1e9a933f4e58658d7b12defcf96dc5c720f20832deebe3e0a19efd3b6aaeeb9e" +"checksum alphanumeric-sort 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "7cd2580c95c654d681db0194a310af67a293f5e1c8bafa5b35b63269c4665a39" "checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" "checksum arc-swap 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "1025aeae2b664ca0ea726a89d574fe8f4e77dd712d443236ad1de00379450cf6" "checksum arrayvec 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)" = "92c7fb76bc8826a8b33b4ee5bb07a247a81e76764ab4d55e8f73e3a4d8808c71" @@ -27,4 +27,5 @@ base64 = "0.10" percent-encoding = "1.0.1" htmlescape = "0.3.1" bytesize = "1.0.0" -nanoid = "0.2.0"
\ No newline at end of file +nanoid = "0.2.0" +alphanumeric-sort = "1.0.6"
\ No newline at end of file @@ -34,6 +34,31 @@ Sometimes this is just a more practical and quick way than doing things properly miniserve -i 192.168.0.1 -i 10.13.37.10 -i ::1 -- /tmp/myshare +### Sort files for easier navigation + miniserve --sort=natural /tmp/myshare # (default behaviour) + # 1/ + # 2/ + # 3 + # 11 + + miniserve --sort=alpha /tmp/myshare + # 1/ + # 11 + # 2/ + # 3 + + miniserve --sort=dirsfirst /tmp/myshare + # 1/ + # 2/ + # 11 + # 3 + + miniserve --reverse /tmp/myshare + # 11 + # 3 + # 2/ + # 1/ + ## Features - Easy to use diff --git a/src/main.rs b/src/main.rs index b7f9be9..d3fb3b1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,10 +6,12 @@ use clap::{crate_authors, crate_description, crate_name, crate_version}; use htmlescape::encode_minimal as escape_html_entity; use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET}; use simplelog::{Config, LevelFilter, TermLogger}; +use std::cmp::Ordering; use std::fmt::Write as FmtWrite; use std::io::{self, Write}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, ToSocketAddrs}; use std::path::{Path, PathBuf}; +use std::str::FromStr; use std::thread; use std::time::Duration; use yansi::{Color, Paint}; @@ -30,6 +32,13 @@ struct BasicAuthParams { } #[derive(Clone, Debug)] +enum SortingMethods { + Natural, + Alpha, + DirsFirst, +} + +#[derive(Clone, Debug)] pub struct MiniserveConfig { verbose: bool, path: std::path::PathBuf, @@ -39,6 +48,60 @@ pub struct MiniserveConfig { path_explicitly_chosen: bool, no_symlinks: bool, random_route: Option<String>, + sort_method: SortingMethods, + reverse_sort: bool, +} + +#[derive(PartialEq)] +enum EntryType { + Directory, + File, +} + +impl PartialOrd for EntryType { + fn partial_cmp(&self, other: &EntryType) -> Option<Ordering> { + 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<bytesize::ByteSize>, +} + +impl Entry { + fn new( + name: String, + entry_type: EntryType, + link: String, + size: Option<bytesize::ByteSize>, + ) -> Self { + Entry { + name, + entry_type, + link, + size, + } + } +} + +impl FromStr for SortingMethods { + type Err = (); + + fn from_str(s: &str) -> Result<SortingMethods, ()> { + match s { + "natural" => Ok(SortingMethods::Natural), + "alpha" => Ok(SortingMethods::Alpha), + "dirsfirst" => Ok(SortingMethods::DirsFirst), + _ => Err(()), + } + } } /// Decode a HTTP basic auth string into a tuple of username and password. @@ -141,6 +204,19 @@ pub fn parse_args() -> MiniserveConfig { .help("Generate a random 6-hexdigit route"), ) .arg( + Arg::with_name("sort") + .short("s") + .long("sort") + .possible_values(&["natural", "alpha", "dirsfirst"]) + .default_value("natural") + .help("Sort files"), + ) + .arg( + Arg::with_name("reverse") + .long("reverse") + .help("Reverse sorting order"), + ) + .arg( Arg::with_name("no-symlinks") .short("P") .long("no-symlinks") @@ -180,6 +256,14 @@ pub fn parse_args() -> MiniserveConfig { None }; + let sort_method = matches + .value_of("sort") + .unwrap() + .parse::<SortingMethods>() + .unwrap(); + + let reverse_sort = matches.is_present("reverse"); + MiniserveConfig { verbose, path: PathBuf::from(path.unwrap_or(".")), @@ -189,6 +273,8 @@ pub fn parse_args() -> MiniserveConfig { path_explicitly_chosen: path.is_some(), no_symlinks, random_route, + sort_method, + reverse_sort, } } @@ -202,6 +288,8 @@ fn configure_app(app: App<MiniserveConfig>) -> App<MiniserveConfig> { let path = &app.state().path; let no_symlinks = app.state().no_symlinks; let random_route = app.state().random_route.clone(); + let sort_method = app.state().sort_method.clone(); + let reverse_sort = app.state().reverse_sort; if path.is_file() { None } else { @@ -210,7 +298,14 @@ fn configure_app(app: App<MiniserveConfig>) -> App<MiniserveConfig> { .expect("Couldn't create path") .show_files_listing() .files_listing_renderer(move |dir, req| { - directory_listing(dir, req, no_symlinks, random_route.clone()) + directory_listing( + dir, + req, + no_symlinks, + random_route.clone(), + sort_method.clone(), + reverse_sort, + ) }), ) } @@ -399,6 +494,8 @@ fn directory_listing<S>( req: &HttpRequest<S>, skip_symlinks: bool, random_route: Option<String>, + sort_method: SortingMethods, + reverse_sort: bool, ) -> Result<HttpResponse, io::Error> { let index_of = format!("Index of {}", req.path()); let mut body = String::new(); @@ -415,6 +512,8 @@ fn directory_listing<S>( } } + let mut entries: Vec<Entry> = Vec::new(); + for entry in dir.path.read_dir()? { if dir.is_visible(&entry) { let entry = entry.unwrap(); @@ -433,21 +532,15 @@ fn directory_listing<S>( if skip_symlinks && metadata.file_type().is_symlink() { continue; } - if metadata.is_dir() { - let _ = write!( - body, - "<tr><td><a class=\"directory\" href=\"{}\">{}/</a></td><td></td></tr>", - file_url, file_name - ); + entries.push(Entry::new(file_name, EntryType::Directory, file_url, None)); } else { - let _ = write!( - body, - "<tr><td><a class=\"file\" href=\"{}\">{}</a></td><td>{}</td></tr>", - file_url, + entries.push(Entry::new( file_name, - ByteSize::b(metadata.len()) - ); + EntryType::File, + file_url, + Some(ByteSize::b(metadata.len())), + )); } } else { continue; @@ -455,6 +548,44 @@ fn directory_listing<S>( } } + 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, + "<tr><td><a class=\"directory\" href=\"{}\">{}/</a></td><td></td></tr>", + entry.link, entry.name + ); + } + EntryType::File => { + let _ = write!( + body, + "<tr><td><a class=\"file\" href=\"{}\">{}</a></td><td>{}</td></tr>", + entry.link, + entry.name, + entry.size.unwrap() + ); + } + } + } + let html = format!( "<html>\ <head>\ @@ -499,7 +630,7 @@ fn directory_listing<S>( a.root, a.root:visited {{\ font-weight: bold;\ color: #777c82;\ - }} + }}\ a.directory {{\ font-weight: bold;\ }}\ |