diff options
Diffstat (limited to '')
-rw-r--r-- | src/listing.rs | 238 | ||||
-rw-r--r-- | src/main.rs | 3 | ||||
-rw-r--r-- | src/renderer.rs | 230 |
3 files changed, 250 insertions, 221 deletions
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() +} |