aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorboasting-squirrel <boasting.squirrel@gmail.com>2019-02-23 10:06:19 +0000
committerboasting-squirrel <boasting.squirrel@gmail.com>2019-02-23 10:06:19 +0000
commit984823e6ef2f19939583edcf7ff82508e13d3fab (patch)
tree70e7392e9482a6fe0ba09818488488762faa02d5
parentMerge pull request #40 from boastful-squirrel/mobile (diff)
downloadminiserve-984823e6ef2f19939583edcf7ff82508e13d3fab.tar.gz
miniserve-984823e6ef2f19939583edcf7ff82508e13d3fab.zip
Renders file listing using maud
Diffstat (limited to '')
-rw-r--r--Cargo.lock35
-rw-r--r--Cargo.toml3
-rw-r--r--README.md2
-rw-r--r--src/listing.rs238
-rw-r--r--src/main.rs3
-rw-r--r--src/renderer.rs230
6 files changed, 288 insertions, 223 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 13bc678..f8caa0e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -656,6 +656,11 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
+name = "literalext"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
name = "lock_api"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -686,6 +691,31 @@ version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
+name = "maud"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "actix-web 0.7.18 (registry+https://github.com/rust-lang/crates.io-index)",
+ "maud_htmlescape 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "maud_macros 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "maud_htmlescape"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "maud_macros"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "literalext 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "maud_htmlescape 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
name = "memchr"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -731,6 +761,7 @@ dependencies = [
"chrono-humanize 0.0.11 (registry+https://github.com/rust-lang/crates.io-index)",
"clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)",
"htmlescape 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "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)",
"simplelog 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1877,10 +1908,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum lazycell 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b294d6fa9ee409a054354afc4352b0b9ef7ca222c69b8812cbea9e7d2bf3783f"
"checksum libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)" = "e962c7641008ac010fa60a7dfdc1712449f29c44ef2d4702394aea943ee75047"
"checksum linked-hash-map 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7860ec297f7008ff7a1e3382d7f7e1dcd69efc94751a2284bafc3d013c2aa939"
+"checksum literalext 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2f42dd699527975a1e0d722e0707998671188a0125f2051d2d192fc201184a81"
"checksum lock_api 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "62ebf1391f6acad60e5c8b43706dde4582df75c06698ab44511d15016bc2442c"
"checksum log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c84ec4b527950aa83a329754b01dbe3f58361d1c5efacd1f6d68c494d08a17c6"
"checksum lru-cache 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4d06ff7ff06f729ce5f4e227876cb88d10bc59cd4ae1e09fbb2bde15c850dc21"
"checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
+"checksum maud 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)" = "337b4b2512ff8809450badd92cf3b529dc6108e333dfa1626971412f8de5793b"
+"checksum maud_htmlescape 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d0fb85bccffc42302ad1e1ed8679f6a39d1317f775a37fbc3f79bdfbe054bfb7"
+"checksum maud_macros 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6f58751cda7f79eedc668ce60e5bcd88dca49e412ec37545a792e2c399fbca41"
"checksum memchr 2.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e1dd4eaac298c32ce07eb6ed9242eda7d82955b9170b7d6db59b2e02cc63fcb8"
"checksum memoffset 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0f9dc261e2b62d7a622bf416ea3c5245cdd5d9a7fcc428c0d06804dfce1775b3"
"checksum mime 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)" = "3e27ca21f40a310bd06d9031785f4801710d566c184a6e15bad4f1d9b65f9425"
diff --git a/Cargo.toml b/Cargo.toml
index 510256a..a8ae9df 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -31,4 +31,5 @@ nanoid = "0.2.0"
alphanumeric-sort = "1.0.6"
structopt = "0.2.14"
chrono = "0.4.6"
-chrono-humanize = "0.0.11" \ No newline at end of file
+chrono-humanize = "0.0.11"
+maud = { version = "0.20.0", features = ["actix-web"] } \ No newline at end of file
diff --git a/README.md b/README.md
index 7c5e149..73dba75 100644
--- a/README.md
+++ b/README.md
@@ -89,7 +89,7 @@ Sometimes this is just a more practical and quick way than doing things properly
miniserve-win.exe
-**With Cargo**: If you have a somewhat recent version of Rust and Cargo installed, you can run
+**With Cargo**: You will need the _nightly_ version of Rust to compile the project. Then you can run
cargo install miniserve
miniserve
diff --git a/src/listing.rs b/src/listing.rs
index 629a117..9af2992 100644
--- a/src/listing.rs
+++ b/src/listing.rs
@@ -1,16 +1,15 @@
use actix_web::{fs, HttpRequest, HttpResponse, Result};
use bytesize::ByteSize;
-use chrono::{DateTime, Duration, Utc};
-use chrono_humanize::{Accuracy, HumanTime, Tense};
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 std::fmt::Write as FmtWrite;
use std::io;
use std::path::Path;
use std::time::SystemTime;
+use crate::renderer;
+
arg_enum! {
#[derive(Clone, Copy, Debug)]
/// Available sorting methods
@@ -35,7 +34,7 @@ arg_enum! {
#[derive(PartialEq)]
/// Possible entry types
-enum EntryType {
+pub enum EntryType {
/// Entry is a directory
Directory,
@@ -54,21 +53,21 @@ impl PartialOrd for EntryType {
}
/// Entry
-struct Entry {
+pub struct Entry {
/// Name of the entry
- name: String,
+ pub name: String,
/// Type of the entry
- entry_type: EntryType,
+ pub entry_type: EntryType,
/// URL of the entry
- link: String,
+ pub link: String,
/// Size in byte of the entry. Only available for EntryType::File
- size: Option<bytesize::ByteSize>,
+ pub size: Option<bytesize::ByteSize>,
/// Last modification date
- last_modification_date: Option<SystemTime>,
+ pub last_modification_date: Option<SystemTime>,
}
impl Entry {
@@ -87,6 +86,10 @@ impl Entry {
last_modification_date,
}
}
+
+ pub fn is_dir(&self) -> bool {
+ self.entry_type == EntryType::Directory
+ }
}
pub fn file_handler(req: &HttpRequest<crate::MiniserveConfig>) -> Result<fs::NamedFile> {
@@ -104,20 +107,11 @@ pub fn directory_listing<S>(
sort_method: SortingMethods,
reverse_sort: bool,
) -> Result<HttpResponse, io::Error> {
- let index_of = format!("Index of {}", req.path());
- let mut body = String::new();
+ let title = format!("Index of {}", req.path());
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,
- "<tr><td><a class=\"root\" href=\"{}\">..</a></td><td></td></tr>",
- parent.display()
- );
- }
- }
+ let is_root = base.parent().is_none() || req.path() == random_route;
+ let page_parent = base.parent().map(|p| p.display().to_string());
let mut entries: Vec<Entry> = Vec::new();
@@ -190,206 +184,8 @@ pub fn directory_listing<S>(
if reverse_sort {
entries.reverse();
}
- for entry in entries {
- let (modification_date, modification_time) = convert_to_utc(entry.last_modification_date);
- match entry.entry_type {
- EntryType::Directory => {
- let _ = write!(
- body,
- "<tr>\
- <td>\
- <a class=\"directory\" href=\"{}\">{}/</a>\
- <span class=\"mobile-info\">\
- <strong>Last modification:</strong> {} {}\
- </span>\
- </td>\
- <td></td>\
- <td class=\"date-cell\">\
- <span>{}</span>\
- <span>{}</span>\
- <span>{}</span>\
- </td>\
- </tr>",
- entry.link,
- entry.name,
- modification_date,
- modification_time,
- modification_date,
- modification_time,
- humanize_systemtime(entry.last_modification_date)
- );
- }
- EntryType::File => {
- let _ = write!(
- body,
- "<tr>\
- <td>\
- <a class=\"file\" href=\"{}\">{}</a>\
- <span class=\"mobile-info\">\
- <strong>Size:</strong> {}\
- </span>\
- <span class=\"mobile-info\">\
- <strong>Last modification:</strong> {} {} <span class=\"history\">({})</span>\
- </span>\
- </td>\
- <td>\
- {}\
- </td>\
- <td class=\"date-cell\">\
- <span>{}</span>\
- <span>{}</span>\
- <span>{}</span>\
- </td>\
- </tr>",
- entry.link,
- entry.name,
- entry.size.unwrap(),
- modification_date,
- modification_time,
- humanize_systemtime(entry.last_modification_date),
- entry.size.unwrap(),
- modification_date,
- modification_time,
- humanize_systemtime(entry.last_modification_date)
- );
- }
- }
- }
-
- let html = format!(
- "<html>\
- <head>\
- <title>{}</title>\
- <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\
- <style>\
- body {{\
- margin: 0;\
- font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto,\"Helvetica Neue\", Helvetica, Arial, sans-serif;\
- font-weight: 300;\
- color: #444444;\
- padding: 0.125rem;\
- }}\
- table {{\
- width: 100%;\
- background: white;\
- border: 0;\
- table-layout: auto;\
- }}\
- table thead {{\
- background: #efefef;\
- }}\
- table tr th,\
- table tr td {{\
- padding: 0.5625rem 0.625rem;\
- font-size: 0.875rem;\
- color: #777c82;\
- text-align: left;\
- line-height: 1.125rem;\
- width: 33.333%;\
- }}\
- table thead tr th {{\
- padding: 0.5rem 0.625rem 0.625rem;\
- font-weight: bold;\
- color: #444444;\
- }}\
- table tr:nth-child(even) {{\
- background: #f6f6f6;\
- }}\
- a {{\
- text-decoration: none;\
- color: #3498db;\
- }}\
- a.root, a.root:visited {{\
- font-weight: bold;\
- color: #777c82;\
- }}\
- a.directory {{\
- font-weight: bold;\
- }}\
- a:hover {{\
- text-decoration: underline;\
- }}\
- a:visited {{\
- color: #8e44ad;\
- }}\
- td.date-cell {{\
- display: flex;\
- width: calc(100% - 1.25rem);\
- }}\
- td.date-cell span:first-of-type,\
- td.date-cell span:nth-of-type(2) {{\
- flex-basis:4.5rem;\
- }}\
- td.date-cell span:nth-of-type(3), .history {{\
- color: #c5c5c5;\
- }}\
- .file, .directory {{\
- display: block;\
- }}\
- .mobile-info {{\
- display: none;\
- }}\
- @media (max-width: 600px) {{\
- h1 {{\
- font-size: 1.375em;\
- }}\
- td:not(:nth-child(1)), th:not(:nth-child(1)){{\
- display: none;\
- }}\
- .mobile-info {{\
- display: block;\
- }}\
- .file, .directory{{\
- padding-bottom: 0.5rem;\
- }}\
- }}\
- @media (max-width: 400px) {{\
- h1 {{\
- font-size: 1.375em;\
- }}\
- }}\
- </style>\
- </head>\
- <body><h1>{}</h1>\
- <table>\
- <thead><th>Name</th><th>Size</th><th>Last modification</th></thead>\
- <tbody>\
- {}\
- </tbody></table></body>\n</html>",
- index_of, index_of, body
- );
Ok(HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
- .body(html))
-}
-
-/// 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
-///
-/// If no SystemTime was given, returns a tuple containing empty strings
-fn convert_to_utc(src_time: Option<SystemTime>) -> (String, String) {
- src_time
- .map(|time| DateTime::<Utc>::from(time))
- .map(|date_time| {
- (
- date_time.format("%e %b").to_string(),
- date_time.format("%R").to_string(),
- )
- })
- .unwrap_or_default()
-}
-
-/// Converts a SystemTime to a string readable by a human,
-/// i.e. calculates the duration between now() and the given SystemTime,
-/// and gives a rough approximation of the elapsed time since
-///
-/// If no SystemTime was given, returns an empty string
-fn humanize_systemtime(src_time: Option<SystemTime>) -> String {
- src_time
- .and_then(|std_time| SystemTime::now().duration_since(std_time).ok())
- .and_then(|from_now| Duration::from_std(from_now).ok())
- .map(|duration| HumanTime::from(duration).to_text_en(Accuracy::Rough, Tense::Past))
- .unwrap_or_default()
+ .body(renderer::page(&title, entries, is_root, page_parent).into_string()))
}
diff --git a/src/main.rs b/src/main.rs
index 378a4d0..e9b16df 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,3 +1,5 @@
+#![feature(proc_macro_hygiene)]
+
use actix_web::{fs, middleware, server, App};
use clap::crate_version;
use simplelog::{Config, LevelFilter, TermLogger};
@@ -10,6 +12,7 @@ use yansi::{Color, Paint};
mod args;
mod auth;
mod listing;
+mod renderer;
#[derive(Clone, Debug)]
/// Configuration of the Miniserve application
diff --git a/src/renderer.rs b/src/renderer.rs
new file mode 100644
index 0000000..4a46940
--- /dev/null
+++ b/src/renderer.rs
@@ -0,0 +1,230 @@
+use chrono::{DateTime, Duration, Utc};
+use chrono_humanize::{Accuracy, HumanTime, Tense};
+use maud::{html, Markup, PreEscaped, DOCTYPE};
+use std::time::SystemTime;
+
+use crate::listing;
+
+/// Renders the file listing
+pub fn page(
+ page_title: &str,
+ entries: Vec<listing::Entry>,
+ is_root: bool,
+ page_parent: Option<String>,
+) -> Markup {
+ html! {
+ (page_header(page_title))
+ body {
+ h1 { (page_title) }
+ table {
+ thead {
+ th { "Name" }
+ th { "Size" }
+ th { "Last modification" }
+ }
+ tbody {
+ @if !is_root {
+ @if let Some(parent) = page_parent {
+ tr {
+ td {
+ a.root href=(parent) {
+ ".."
+ }
+ }
+ }
+ }
+ }
+ @for entry in entries {
+ (entry_row(entry))
+ }
+ }
+ }
+ }
+ }
+}
+
+/// 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()) }
+ }
+ }
+}
+
+/// Partial: row for an entry
+fn entry_row(entry: listing::Entry) -> Markup {
+ html! {
+ @let (modification_date, modification_time) = convert_to_utc(entry.last_modification_date);
+ @let last_modification_timer = humanize_systemtime(entry.last_modification_date);
+ tr {
+ td {
+ @if entry.is_dir() {
+ a.directory href=(entry.link) {
+ (entry.name) "/"
+ }
+ } @else {
+ a.file href=(entry.link) {
+ (entry.name)
+ }
+ }
+ @if !entry.is_dir() {
+ @if let Some(size) = entry.size {
+ span .mobile-info {
+ strong { "Size: " }
+ (size)
+ }
+ }
+ }
+ span .mobile-info {
+ strong { "Last modification: " }
+ (modification_date) " "
+ (modification_time) " "
+ span .history { "(" (last_modification_timer) ")" }
+ }
+ }
+ td {
+ @if let Some(size) = entry.size {
+ (size)
+ }
+ }
+ td.date-cell {
+ span {
+ (modification_date)
+ }
+ span {
+ (modification_time)
+ }
+ span {
+ "(" (last_modification_timer) ")"
+ }
+ }
+ }
+ }
+}
+
+/// Partial: CSS
+fn css() -> Markup {
+ (PreEscaped(r#"
+ body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,"Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-weight: 300;
+ color: #444444;
+ padding: 0.125rem;
+ }
+ table {
+ width: 100%;
+ background: white;
+ border: 0;
+ table-layout: auto;
+ }
+ table thead {
+ background: #efefef;
+ }
+ table tr th,
+ table tr td {
+ padding: 0.5625rem 0.625rem;
+ font-size: 0.875rem;
+ color: #777c82;
+ text-align: left;
+ line-height: 1.125rem;
+ width: 33.333%;
+ }
+ table thead tr th {
+ padding: 0.5rem 0.625rem 0.625rem;
+ font-weight: bold;
+ color: #444444;
+ }
+ table tr:nth-child(even) {
+ background: #f6f6f6;
+ }
+ a {
+ text-decoration: none;
+ color: #3498db;
+ }
+ a.root, a.root:visited {
+ font-weight: bold;
+ color: #777c82;
+ }
+ a.directory {
+ font-weight: bold;
+ }
+ a:hover {
+ text-decoration: underline;
+ }
+ a:visited {
+ color: #8e44ad;
+ }
+ td.date-cell {
+ display: flex;
+ width: calc(100% - 1.25rem);
+ }
+ td.date-cell span:first-of-type,
+ td.date-cell span:nth-of-type(2) {
+ flex-basis:4.5rem;
+ }
+ td.date-cell span:nth-of-type(3), .history {
+ color: #c5c5c5;
+ }
+ .file, .directory {
+ display: block;
+ }
+ .mobile-info {
+ display: none;
+ }
+ @media (max-width: 600px) {
+ h1 {
+ font-size: 1.375em;
+ }
+ td:not(:nth-child(1)), th:not(:nth-child(1)){
+ display: none;
+ }
+ .mobile-info {
+ display: block;
+ }
+ .file, .directory{
+ padding-bottom: 0.5rem;
+ }
+ }
+ @media (max-width: 400px) {
+ h1 {
+ font-size: 1.375em;
+ }
+ }"#.to_string()))
+}
+
+/// 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
+///
+/// If no SystemTime was given, returns a tuple containing empty strings
+fn convert_to_utc(src_time: Option<SystemTime>) -> (String, String) {
+ src_time
+ .map(DateTime::<Utc>::from)
+ .map(|date_time| {
+ (
+ date_time.format("%e %b").to_string(),
+ date_time.format("%R").to_string(),
+ )
+ })
+ .unwrap_or_default()
+}
+
+/// Converts a SystemTime to a string readable by a human,
+/// i.e. calculates the duration between now() and the given SystemTime,
+/// and gives a rough approximation of the elapsed time since
+///
+/// If no SystemTime was given, returns an empty string
+fn humanize_systemtime(src_time: Option<SystemTime>) -> String {
+ src_time
+ .and_then(|std_time| SystemTime::now().duration_since(std_time).ok())
+ .and_then(|from_now| Duration::from_std(from_now).ok())
+ .map(|duration| HumanTime::from(duration).to_text_en(Accuracy::Rough, Tense::Past))
+ .unwrap_or_default()
+}