aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock27
-rw-r--r--Cargo.toml1
-rw-r--r--src/args.rs18
-rw-r--r--src/listing.rs157
-rw-r--r--src/main.rs17
-rw-r--r--src/renderer.rs163
6 files changed, 262 insertions, 121 deletions
diff --git a/Cargo.lock b/Cargo.lock
index c4004bc..6bec0bf 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -86,8 +86,8 @@ dependencies = [
"parking_lot 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
"percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "regex 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)",
+ "regex 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_urlencoded 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)",
"sha1 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -760,6 +760,7 @@ dependencies = [
"maud 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)",
"nanoid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)",
"simplelog 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
"structopt 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)",
"yansi 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1199,8 +1200,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "serde"
-version = "1.0.88"
+version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "serde_derive 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)",
+]
[[package]]
name = "serde_json"
@@ -1209,7 +1223,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
"ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -1219,7 +1233,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"dtoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
"itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)",
"url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -1950,7 +1964,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27"
"checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
"checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
-"checksum serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)" = "9f301d728f2b94c9a7691c90f07b0b4e8a4517181d9461be94c04bddeb4bd850"
+"checksum serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)" = "92514fb95f900c9b5126e32d020f5c6d40564c27a5ea6d1d7d9f157a96623560"
+"checksum serde_derive 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)" = "bb6eabf4b5914e88e24eea240bb7c9f9a2cbc1bbbe8d961d381975ec3c6b806c"
"checksum serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)" = "27dce848e7467aa0e2fcaf0a413641499c0b745452aaca1194d24dedde9e13c9"
"checksum serde_urlencoded 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)" = "d48f9f99cd749a2de71d29da5f948de7f2764cc5a9d7f3c97e3514d4ee6eabf2"
"checksum sha1 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d"
diff --git a/Cargo.toml b/Cargo.toml
index fc5c17e..f98e802 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -36,3 +36,4 @@ structopt = "0.2.14"
chrono = "0.4.6"
chrono-humanize = "0.0.11"
maud = { version = "0.20.0", features = ["actix-web"] }
+serde = { version = "1.0.89", features = ["derive"] }
diff --git a/src/args.rs b/src/args.rs
index 637c224..4f0dbf7 100644
--- a/src/args.rs
+++ b/src/args.rs
@@ -3,7 +3,6 @@ use std::path::PathBuf;
use structopt::StructOpt;
use crate::auth;
-use crate::listing;
/// Possible characters for random routes
const ROUTE_ALPHABET: [char; 16] = [
@@ -45,21 +44,6 @@ struct CLIArgs {
#[structopt(long = "random-route")]
random_route: bool,
- /// Sort files
- #[structopt(
- short = "s",
- long = "sort",
- raw(
- possible_values = "&listing::SortingMethods::variants()",
- case_insensitive = "true"
- )
- )]
- sort_method: Option<listing::SortingMethods>,
-
- /// Reverse sorting
- #[structopt(long = "reverse")]
- reverse_sort: bool,
-
/// Do not follow symbolic links
#[structopt(short = "P", long = "no-symlinks")]
no_symlinks: bool,
@@ -116,7 +100,5 @@ pub fn parse_args() -> crate::MiniserveConfig {
path_explicitly_chosen,
no_symlinks: args.no_symlinks,
random_route,
- sort_method: args.sort_method.unwrap_or(listing::SortingMethods::Natural),
- reverse_sort: args.reverse_sort,
}
}
diff --git a/src/listing.rs b/src/listing.rs
index 9af2992..e86cc04 100644
--- a/src/listing.rs
+++ b/src/listing.rs
@@ -1,34 +1,67 @@
-use actix_web::{fs, HttpRequest, HttpResponse, Result};
+use actix_web::{fs, FromRequest, HttpRequest, HttpResponse, Query, Result};
use bytesize::ByteSize;
-use clap::{_clap_count_exprs, arg_enum};
use htmlescape::encode_minimal as escape_html_entity;
use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET};
-use std::cmp::Ordering;
+use serde::Deserialize;
use std::io;
use std::path::Path;
use std::time::SystemTime;
use crate::renderer;
-arg_enum! {
- #[derive(Clone, Copy, Debug)]
- /// Available sorting methods
- ///
- /// Natural: natural sorting method
- /// 1 -> 2 -> 3 -> 11
- ///
- /// Alpha: pure alphabetical sorting method
- /// 1 -> 11 -> 2 -> 3
- ///
- /// DirsFirst: directories are listed first, alphabetical sorting is also applied
- /// 1/ -> 2/ -> 3/ -> 11 -> 12
- ///
- /// Date: sort by last modification date (most recent first)
- pub enum SortingMethods {
- Natural,
- Alpha,
- DirsFirst,
- Date
+/// Query parameters
+#[derive(Debug, Deserialize)]
+struct QueryParameters {
+ sort: Option<SortingMethod>,
+ order: Option<SortingOrder>,
+}
+
+/// Available sorting methods
+#[derive(Debug, Deserialize, Clone)]
+pub enum SortingMethod {
+ /// Sort by name
+ #[serde(alias = "name")]
+ Name,
+
+ /// Sort by size
+ #[serde(alias = "size")]
+ Size,
+
+ /// Sort by last modification date (natural sort: follows alphanumerical order)
+ #[serde(alias = "date")]
+ Date,
+}
+
+impl SortingMethod {
+ pub fn to_string(&self) -> String {
+ match &self {
+ SortingMethod::Name => "name",
+ SortingMethod::Size => "size",
+ SortingMethod::Date => "date",
+ }
+ .to_string()
+ }
+}
+
+/// Available sorting orders
+#[derive(Debug, Deserialize, Clone)]
+pub enum SortingOrder {
+ /// Ascending order
+ #[serde(alias = "asc")]
+ Ascending,
+
+ /// Descending order
+ #[serde(alias = "desc")]
+ Descending,
+}
+
+impl SortingOrder {
+ pub fn to_string(&self) -> String {
+ match &self {
+ SortingOrder::Ascending => "asc",
+ SortingOrder::Descending => "desc",
+ }
+ .to_string()
}
}
@@ -42,16 +75,6 @@ pub enum EntryType {
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),
- }
- }
-}
-
/// Entry
pub struct Entry {
/// Name of the entry
@@ -104,8 +127,6 @@ pub 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 title = format!("Index of {}", req.path());
let base = Path::new(req.path());
@@ -113,6 +134,14 @@ pub fn directory_listing<S>(
let is_root = base.parent().is_none() || req.path() == random_route;
let page_parent = base.parent().map(|p| p.display().to_string());
+ let mut sort_method: Option<SortingMethod> = None;
+ let mut sort_order: Option<SortingOrder> = None;
+
+ if let Ok(query) = Query::<QueryParameters>::extract(req) {
+ sort_method = query.sort.clone();
+ sort_order = query.order.clone();
+ }
+
let mut entries: Vec<Entry> = Vec::new();
for entry in dir.path.read_dir()? {
@@ -161,31 +190,47 @@ pub 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 let Some(sorting_method) = &sort_method {
+ match sorting_method {
+ SortingMethod::Name => entries
+ .sort_by(|e1, e2| alphanumeric_sort::compare_str(e1.name.clone(), e2.name.clone())),
+ SortingMethod::Size => entries.sort_by(|e1, e2| {
+ // If we can't get the size of the entry (directory for instance)
+ // let's consider it's 0b
+ e2.size
+ .unwrap_or_else(|| ByteSize::b(0))
+ .cmp(&e1.size.unwrap_or_else(|| ByteSize::b(0)))
+ }),
+ SortingMethod::Date => entries.sort_by(|e1, e2| {
+ // If, for some reason, we can't get the last modification date of an entry
+ // let's consider it was modified on UNIX_EPOCH (01/01/19270 00:00:00)
+ e2.last_modification_date
+ .unwrap_or(SystemTime::UNIX_EPOCH)
+ .cmp(&e1.last_modification_date.unwrap_or(SystemTime::UNIX_EPOCH))
+ }),
+ };
+ } else {
+ // Sort in alphanumeric order by default
+ entries.sort_by(|e1, e2| alphanumeric_sort::compare_str(e1.name.clone(), e2.name.clone()))
+ }
+
+ if let Some(sorting_order) = &sort_order {
+ if let SortingOrder::Descending = sorting_order {
+ entries.reverse()
}
- SortingMethods::Date => entries.sort_by(|e1, e2| {
- // If, for some reason, we can't get the last modification date of an entry
- // let's consider it was modified on UNIX_EPOCH (01/01/19270 00:00:00)
- e2.last_modification_date
- .unwrap_or(SystemTime::UNIX_EPOCH)
- .cmp(&e1.last_modification_date.unwrap_or(SystemTime::UNIX_EPOCH))
- }),
- };
-
- if reverse_sort {
- entries.reverse();
}
Ok(HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
- .body(renderer::page(&title, entries, is_root, page_parent).into_string()))
+ .body(
+ renderer::page(
+ &title,
+ entries,
+ is_root,
+ page_parent,
+ sort_method,
+ sort_order,
+ )
+ .into_string(),
+ ))
}
diff --git a/src/main.rs b/src/main.rs
index f00fb89..b15088c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -40,12 +40,6 @@ pub struct MiniserveConfig {
/// Enable random route generation
pub random_route: Option<String>,
-
- /// Sort files/directories
- pub sort_method: listing::SortingMethods,
-
- /// Enable inverse sorting
- pub reverse_sort: bool,
}
fn main() {
@@ -181,8 +175,6 @@ 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;
- let reverse_sort = app.state().reverse_sort;
if path.is_file() {
None
} else {
@@ -191,14 +183,7 @@ fn configure_app(app: App<MiniserveConfig>) -> App<MiniserveConfig> {
.expect("Couldn't create path")
.show_files_listing()
.files_listing_renderer(move |dir, req| {
- listing::directory_listing(
- dir,
- req,
- no_symlinks,
- random_route.clone(),
- sort_method,
- reverse_sort,
- )
+ listing::directory_listing(dir, req, no_symlinks, random_route.clone())
}),
)
}
diff --git a/src/renderer.rs b/src/renderer.rs
index 82937ab..89a9248 100644
--- a/src/renderer.rs
+++ b/src/renderer.rs
@@ -11,24 +11,28 @@ pub fn page(
entries: Vec<listing::Entry>,
is_root: bool,
page_parent: Option<String>,
+ sort_method: Option<listing::SortingMethod>,
+ sort_order: Option<listing::SortingOrder>,
) -> Markup {
html! {
(page_header(page_title))
body {
+ span #top { }
h1 { (page_title) }
table {
thead {
- th { "Name" }
- th { "Size" }
- th { "Last modification" }
+ th { (build_link("name", "Name", &sort_method, &sort_order)) }
+ th { (build_link("size", "Size", &sort_method, &sort_order)) }
+ th { (build_link("date", "Last modification", &sort_method, &sort_order)) }
}
tbody {
@if !is_root {
@if let Some(parent) = page_parent {
tr {
- td {
+ td colspan="3" {
+ span.chevron { (chevron_left()) }
a.root href=(parent) {
- ".."
+ "Parent directory"
}
}
}
@@ -39,20 +43,42 @@ pub fn page(
}
}
}
+ a.back href="#top" {
+ (arrow_up())
+ }
}
}
}
-/// Partial: page header
-fn page_header(page_title: &str) -> Markup {
+/// Partial: table header link
+fn build_link(
+ name: &str,
+ title: &str,
+ sort_method: &Option<listing::SortingMethod>,
+ sort_order: &Option<listing::SortingOrder>,
+) -> Markup {
+ let mut link = format!("?sort={}&order=asc", name);
+ let mut help = format!("Sort by {} in ascending order", name);
+ let mut chevron = chevron_up();
+ let mut class = "";
+
+ if let Some(method) = sort_method {
+ if method.to_string() == name {
+ class = "active";
+ if let Some(order) = sort_order {
+ if order.to_string() == "asc" {
+ link = format!("?sort={}&order=desc", name);
+ help = format!("Sort by {} in descending order", name);
+ chevron = chevron_down();
+ }
+ }
+ }
+ };
+
html! {
- (DOCTYPE)
- html {
- meta charset="utf-8";
- meta http-equiv="X-UA-Compatible" content="IE=edge";
- meta name="viewport" content="width=device-width, initial-scale=1";
- title { (page_title) }
- style { (css()) }
+ span class=(class) {
+ span.chevron { (chevron) }
+ a href=(link) title=(help) { (title) }
}
}
}
@@ -62,13 +88,15 @@ fn entry_row(entry: listing::Entry) -> Markup {
html! {
tr {
td {
- @if entry.is_dir() {
- a.directory href=(entry.link) {
- (entry.name) "/"
- }
- } @else {
- a.file href=(entry.link) {
- (entry.name)
+ p {
+ @if entry.is_dir() {
+ a.directory href=(entry.link) {
+ (entry.name) "/"
+ }
+ } @else {
+ a.file href=(entry.link) {
+ (entry.name)
+ }
}
}
@if !entry.is_dir() {
@@ -76,6 +104,7 @@ fn entry_row(entry: listing::Entry) -> Markup {
span .mobile-info {
strong { "Size: " }
(size)
+ (br())
}
}
}
@@ -87,6 +116,7 @@ fn entry_row(entry: listing::Entry) -> Markup {
}
@if let Some(modification_timer) = humanize_systemtime(entry.last_modification_date) {
span .history { "(" (modification_timer) ")" }
+ (br())
}
}
@@ -99,10 +129,10 @@ fn entry_row(entry: listing::Entry) -> Markup {
td.date-cell {
@if let Some(modification_date) = convert_to_utc(entry.last_modification_date) {
span {
- (modification_date.0)
+ (modification_date.0) " "
}
span {
- (modification_date.1)
+ (modification_date.1) " "
}
}
@if let Some(modification_timer) = humanize_systemtime(entry.last_modification_date) {
@@ -118,6 +148,10 @@ fn entry_row(entry: listing::Entry) -> Markup {
/// Partial: CSS
fn css() -> Markup {
(PreEscaped(r#"
+ html {
+ font-smoothing: antialiased;
+ text-rendering: optimizeLegibility;
+ }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,"Helvetica Neue", Helvetica, Arial, sans-serif;
@@ -125,7 +159,15 @@ fn css() -> Markup {
color: #444444;
padding: 0.125rem;
}
+ strong {
+ font-weight: bold;
+ }
+ p {
+ margin: 0;
+ padding: 0;
+ }
table {
+ margin-top: 2rem;
width: 100%;
background: white;
border: 0;
@@ -143,7 +185,7 @@ fn css() -> Markup {
line-height: 1.125rem;
width: 33.333%;
}
- table thead tr th {
+ table tr th {
padding: 0.5rem 0.625rem 0.625rem;
font-weight: bold;
color: #444444;
@@ -151,6 +193,9 @@ fn css() -> Markup {
table tr:nth-child(even) {
background: #f6f6f6;
}
+ table tr:hover {
+ background: #deeef7a6;
+ }
a {
text-decoration: none;
color: #3498db;
@@ -185,6 +230,35 @@ fn css() -> Markup {
.mobile-info {
display: none;
}
+ th a, th a:visited, .chevron {
+ color: #777c82;
+ }
+ .chevron {
+ margin-right: .5rem;
+ font-size: 1.2em;
+ font-weight: bold;
+ }
+ th span.active a, th span.active span {
+ color: #444444;
+ }
+ .back {
+ position: fixed;
+ bottom: 1.1rem;
+ right: 0.625rem;
+ background: #e0e0e0;
+ border-radius: 100%;
+ box-shadow: 0 0 8px -4px #888888;
+ opacity: 0.8;
+ padding: 1rem 1.1rem;
+ color: #444444;
+ }
+ .back:visited {
+ color: #444444;
+ }
+ .back:hover {
+ color: #3498db;
+ text-decoration: none;
+ }
@media (max-width: 600px) {
h1 {
font-size: 1.375em;
@@ -196,7 +270,7 @@ fn css() -> Markup {
display: block;
}
.file, .directory{
- padding-bottom: 0.5rem;
+ padding-bottom: 1rem;
}
}
@media (max-width: 400px) {
@@ -206,6 +280,45 @@ fn css() -> Markup {
}"#.to_string()))
}
+/// Partial: up arrow
+fn arrow_up() -> Markup {
+ (PreEscaped("⇪".to_string()))
+}
+
+/// Partial: new line
+fn br() -> Markup {
+ (PreEscaped("<br>".to_string()))
+}
+
+/// Partial: chevron left
+fn chevron_left() -> Markup {
+ (PreEscaped("◂".to_string()))
+}
+
+/// Partial: chevron up
+fn chevron_up() -> Markup {
+ (PreEscaped("▴".to_string()))
+}
+
+/// Partial: chevron up
+fn chevron_down() -> Markup {
+ (PreEscaped("▾".to_string()))
+}
+
+/// Partial: page header
+fn page_header(page_title: &str) -> Markup {
+ html! {
+ (DOCTYPE)
+ html {
+ meta charset="utf-8";
+ meta http-equiv="X-UA-Compatible" content="IE=edge";
+ meta name="viewport" content="width=device-width, initial-scale=1";
+ title { (page_title) }
+ style { (css()) }
+ }
+ }
+}
+
/// Converts a SystemTime object to a strings tuple (date, time)
/// Date is formatted as %e %b, e.g. Jul 12
/// Time is formatted as %R, e.g. 22:34