Renders file listing using maud

This commit is contained in:
boasting-squirrel 2019-02-23 11:06:19 +01:00
parent f7b0d5bd03
commit 984823e6ef
6 changed files with 288 additions and 223 deletions

35
Cargo.lock generated
View File

@ -655,6 +655,11 @@ name = "linked-hash-map"
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"
@ -685,6 +690,31 @@ name = "matches"
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"
@ -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"

View File

@ -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"
chrono-humanize = "0.0.11"
maud = { version = "0.20.0", features = ["actix-web"] }

View File

@ -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

View File

@ -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()))
}

View File

@ -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

230
src/renderer.rs Normal file
View File

@ -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()
}