diff --git a/src/listing.rs b/src/listing.rs index a62c96984..e36036836 100644 --- a/src/listing.rs +++ b/src/listing.rs @@ -163,12 +163,12 @@ pub fn directory_listing( let base = Path::new(serve_path); let random_route_abs = format!("/{}", conf.route_prefix); - let abs_url = format!( - "{}://{}{}", - req.connection_info().scheme(), - req.connection_info().host(), - req.uri() - ); + let abs_uri = http::Uri::builder() + .scheme(req.connection_info().scheme()) + .authority(req.connection_info().host()) + .path_and_query(req.uri().to_string()) + .build() + .unwrap(); let is_root = base.parent().is_none() || Path::new(&req.path()) == Path::new(&random_route_abs); let encoded_dir = match base.strip_prefix(random_route_abs) { @@ -379,7 +379,7 @@ pub fn directory_listing( renderer::page( entries, readme, - abs_url, + &abs_uri, is_root, query_params, &breadcrumbs, diff --git a/src/renderer.rs b/src/renderer.rs index 60d63f79a..90b00fe0e 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -9,6 +9,7 @@ use fast_qr::{ qr::QRCodeError, QRBuilder, }; +use http::Uri; use maud::{html, Markup, PreEscaped, DOCTYPE}; use strum::{Display, IntoEnumIterator}; @@ -22,7 +23,7 @@ use crate::{archive::ArchiveMethod, MiniserveConfig}; pub fn page( entries: Vec, readme: Option<(String, String)>, - abs_url: impl AsRef, + abs_uri: &Uri, is_root: bool, query_params: QueryParameters, breadcrumbs: &[Breadcrumb], @@ -100,7 +101,7 @@ pub fn page( } } nav { - (qr_spoiler(conf.show_qrcode, abs_url)) + (qr_spoiler(conf.show_qrcode, abs_uri)) (color_scheme_selector(conf.hide_theme_selector)) } div.container { @@ -193,7 +194,7 @@ pub fn page( } div.footer { @if conf.show_wget_footer { - (wget_footer(&title_path, current_user)) + (wget_footer(abs_uri, conf.title.as_deref(), current_user.map(|x| &*x.name))) } @if !conf.hide_version_footer { (version_footer()) @@ -240,8 +241,8 @@ pub fn raw(entries: Vec, is_root: bool) -> Markup { } /// Renders the QR code SVG -fn qr_code_svg(url: impl AsRef, margin: usize) -> Result { - let qr = QRBuilder::new(url.as_ref().to_string()) +fn qr_code_svg(url: &Uri, margin: usize) -> Result { + let qr = QRBuilder::new(url.to_string()) .ecl(consts::QR_EC_LEVEL) .build()?; let svg = SvgBuilder::default().margin(margin).to_str(&qr); @@ -267,26 +268,39 @@ fn version_footer() -> Markup { } } -fn wget_footer(title_path: &str, current_user: Option<&CurrentUser>) -> Markup { - let count = { - let count_slashes = title_path.matches('/').count(); - if count_slashes > 0 { - count_slashes - 1 - } else { - 0 - } +fn wget_footer(abs_path: &Uri, root_dir_name: Option<&str>, current_user: Option<&str>) -> Markup { + fn escape_apostrophes(x: &str) -> String { + x.replace('\'', "'\"'\"'") + } + + // Directory depth, 0 is root directory + let cut_dirs = match abs_path.path().matches('/').count() - 1 { + // Put all the files in a folder of this name + 0 => format!( + " -P '{}'", + escape_apostrophes( + root_dir_name.unwrap_or_else(|| abs_path.authority().unwrap().as_str()) + ) + ), + 1 => String::new(), + // Avoids putting the files in excessive directories + x => format!(" --cut-dirs={}", x - 1), }; - let user_params = if let Some(user) = current_user { - format!(" --ask-password --user {}", user.name) - } else { - "".to_string() + // Ask for password if authentication is required + let user_params = match current_user { + Some(user) => format!(" --ask-password --user '{}'", escape_apostrophes(user)), + None => String::new(), }; + let command = + format!("wget -rcnHp -R 'index.html*'{cut_dirs}{user_params} '{abs_path}?raw=true'"); + let click_to_copy = format!("navigator.clipboard.writeText(\"{command}\")"); + html! { div.downloadDirectory { p { "Download folder:" } - div.cmd { (format!("wget -r -c -nH -np --cut-dirs={count} -R \"index.html*\"{user_params} \"http://{title_path}/?raw=true\"")) } + a.cmd title="Click to copy!" style="cursor: pointer;" onclick=(click_to_copy) { (command) } } } } @@ -335,14 +349,14 @@ pub enum ThemeSlug { } /// Partial: qr code spoiler -fn qr_spoiler(show_qrcode: bool, content: impl AsRef) -> Markup { +fn qr_spoiler(show_qrcode: bool, content: &Uri) -> Markup { html! { @if show_qrcode { div { p { "QR code" } - div.qrcode #qrcode title=(PreEscaped(content.as_ref())) { + div.qrcode #qrcode title=(PreEscaped(content.to_string())) { @match qr_code_svg(content, consts::SVG_QR_MARGIN) { Ok(svg) => (PreEscaped(svg)), Err(err) => (format!("QR generation error: {err:?}")), @@ -683,3 +697,57 @@ pub fn render_error( } } } + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + #[test] + fn test_wget_footer() { + fn to_html(x: &str) -> String { + format!( + r#""#, + x + ) + } + + fn uri(x: &str) -> Uri { + Uri::try_from(x).unwrap() + } + + let to_be_tested: String = wget_footer( + &uri("https://github.com/svenstaro/miniserve/"), + Some("Miniserve"), + None, + ) + .into(); + let solution = to_html("--cut-dirs=1 'https://github.com/svenstaro/miniserve"); + assert_eq!(to_be_tested, solution); + + let to_be_tested: String = wget_footer(&uri("https://github.com/"), None, None).into(); + let solution = to_html("-P 'github.com' 'https://github.com"); + assert_eq!(to_be_tested, solution); + + let to_be_tested: String = wget_footer( + &uri("http://1und1.de/"), + Some("1&1 - Willkommen!!!"), + Some("Marcell D'Avis"), + ) + .into(); + let solution = to_html("-P '1&1 - Willkommen!!!' --ask-password --user 'Marcell D'"'"'Avis' 'http://1und1.de"); + assert_eq!(to_be_tested, solution); + + let to_be_tested: String = wget_footer( + &uri("http://127.0.0.1:1234/geheime_dokumente.php/"), + Some("Streng Geheim!!!"), + Some("uøý`¶'7ÅÛé"), + ) + .into(); + let solution = to_html("--ask-password --user 'uøý`¶'"'"'7ÅÛé' 'http://127.0.0.1:1234/geheime_dokumente.php"); + assert_eq!(to_be_tested, solution); + + let to_be_tested: String = wget_footer(&uri("http://127.0.0.1:420/"), None, None).into(); + let solution = to_html("-P '127.0.0.1:420' 'http://127.0.0.1:420"); + assert_eq!(to_be_tested, solution); + } +} diff --git a/tests/raw.rs b/tests/raw.rs index f212f58b7..95100d2a1 100644 --- a/tests/raw.rs +++ b/tests/raw.rs @@ -11,7 +11,16 @@ use select::predicate::Class; use select::predicate::Name; /// The footer displays the correct wget command to download the folder recursively -#[rstest(depth, dir, case(0, ""), case(2, "very/deeply/nested/"))] +// This test can't test all aspects of the wget footer, +// a more detailed unit test is available +#[rstest( + depth, + dir, + case(0, ""), + case(1, "dira/"), + case(2, "very/deeply/"), + case(3, "very/deeply/nested/") +)] fn ui_displays_wget_element( depth: u8, dir: &str, @@ -32,11 +41,18 @@ fn ui_displays_wget_element( .next() .unwrap() .text(); + let cut_dirs = match depth { + // Put all the files in a folder of this name + 0 => format!(" -P 'localhost:{}'", server.port()), + 1 => String::new(), + // Avoids putting the files in excessive directories + x => format!(" --cut-dirs={}", x - 1), + }; assert_eq!( wget_url, format!( - "wget -r -c -nH -np --cut-dirs={} -R \"index.html*\" \"{}{}?raw=true\"", - depth, + "wget -rcnHp -R 'index.html*'{} '{}{}?raw=true'", + cut_dirs, server.url(), dir )