diff options
-rw-r--r-- | Cargo.lock | 7 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | README.md | 1 | ||||
-rw-r--r-- | src/args.rs | 5 | ||||
-rw-r--r-- | src/listing.rs | 23 | ||||
-rw-r--r-- | src/main.rs | 5 | ||||
-rw-r--r-- | src/renderer.rs | 135 | ||||
-rw-r--r-- | tests/qrcode.rs | 88 |
8 files changed, 208 insertions, 57 deletions
@@ -1753,6 +1753,7 @@ dependencies = [ "percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "port_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "pretty_assertions 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", + "qrcodegen 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.3.9 (registry+https://github.com/rust-lang/crates.io-index)", "reqwest 0.10.6 (registry+https://github.com/rust-lang/crates.io-index)", "rstest 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2232,6 +2233,11 @@ dependencies = [ ] [[package]] +name = "qrcodegen" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] name = "quick-error" version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3888,6 +3894,7 @@ dependencies = [ "checksum proc-macro-nested 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a" "checksum proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" "checksum proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)" = "beae6331a816b1f65d04c45b078fd8e6c93e8071771f41b8163255bbd8d7c8fa" +"checksum qrcodegen 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d24ea38b2345f15533e6668104bec0136883404287e095f15f9ea2522e2b4b6c" "checksum quick-error 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" "checksum quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" "checksum quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" @@ -48,6 +48,7 @@ strum_macros = "0.18.0" sha2 = "0.9" hex = "0.4.2" zip = "0.5.5" +qrcodegen = "1.6.0" [dev-dependencies] assert_cmd = "1.0" @@ -51,6 +51,7 @@ Sometimes this is just a more practical and quick way than doing things properly - Folder download (compressed on the fly as `.tar.gz` or `.zip`) - File uploading - Pretty themes +- Scan QR code for quick access ## How to install diff --git a/src/args.rs b/src/args.rs index a5bcfea..bf16291 100644 --- a/src/args.rs +++ b/src/args.rs @@ -77,6 +77,10 @@ struct CLIArgs { )] color_scheme: themes::ColorScheme, + /// Enable QR code display + #[structopt(short = "q", long = "qrcode")] + qrcode: bool, + /// Enable file uploading #[structopt(short = "u", long = "upload-files")] file_upload: bool, @@ -188,6 +192,7 @@ pub fn parse_args() -> crate::MiniserveConfig { default_color_scheme, index: args.index, overwrite_files: args.overwrite_files, + show_qrcode: args.qrcode, file_upload: args.file_upload, tar_enabled: args.enable_tar, zip_enabled: args.enable_zip, diff --git a/src/listing.rs b/src/listing.rs index 9ba596a..6181c3a 100644 --- a/src/listing.rs +++ b/src/listing.rs @@ -9,6 +9,7 @@ use std::io; use std::path::{Path, PathBuf}; use std::time::SystemTime; use strum_macros::{Display, EnumString}; +use qrcodegen::{QrCode, QrCodeEcc}; use crate::archive::CompressionMethod; use crate::errors::{self, ContextualError}; @@ -24,6 +25,7 @@ pub struct QueryParameters { pub sort: Option<SortingMethod>, pub order: Option<SortingOrder>, pub theme: Option<ColorScheme>, + qrcode: Option<String>, download: Option<CompressionMethod>, } @@ -135,6 +137,7 @@ pub fn directory_listing<S>( file_upload: bool, random_route: Option<String>, default_color_scheme: ColorScheme, + show_qrcode: bool, upload_route: String, tar_enabled: bool, zip_enabled: bool, @@ -163,6 +166,23 @@ pub fn directory_listing<S>( let query_params = extract_query_parameters(req); + // If the `qrcode` parameter is included in the url, then should respond to the QR code + if let Some(url) = query_params.qrcode { + let res = match QrCode::encode_text(&url, QrCodeEcc::Medium) { + Ok(qr) => { + HttpResponse::Ok() + .header("Content-Type", "image/svg+xml") + .body(qr.to_svg_string(2)) + }, + Err(err) => { + log::error!("URL is too long: {:?}", err); + HttpResponse::UriTooLong() + .body(Body::Empty) + } + }; + return Ok(res) + } + let mut entries: Vec<Entry> = Vec::new(); for entry in dir.path.read_dir()? { @@ -330,6 +350,7 @@ pub fn directory_listing<S>( query_params.order, default_color_scheme, color_scheme, + show_qrcode, file_upload, &upload_route, ¤t_dir.display().to_string(), @@ -348,6 +369,7 @@ pub fn extract_query_parameters<S>(req: &HttpRequest<S>) -> QueryParameters { order: query.order, download: query.download, theme: query.theme, + qrcode: query.qrcode.to_owned(), path: query.path.clone(), }, Err(e) => { @@ -358,6 +380,7 @@ pub fn extract_query_parameters<S>(req: &HttpRequest<S>) -> QueryParameters { order: None, download: None, theme: None, + qrcode: None, path: None, } } diff --git a/src/main.rs b/src/main.rs index 3ff35c8..a9e7944 100644 --- a/src/main.rs +++ b/src/main.rs @@ -57,6 +57,9 @@ pub struct MiniserveConfig { /// However, if a directory contains this file, miniserve will serve that file instead. pub index: Option<std::path::PathBuf>, + /// Enable QR code display + pub show_qrcode: bool, + /// Enable file upload pub file_upload: bool, @@ -258,6 +261,7 @@ fn configure_app(app: App<MiniserveConfig>) -> App<MiniserveConfig> { let no_symlinks = app.state().no_symlinks; let random_route = app.state().random_route.clone(); let default_color_scheme = app.state().default_color_scheme; + let show_qrcode = app.state().show_qrcode; let file_upload = app.state().file_upload; let tar_enabled = app.state().tar_enabled; let zip_enabled = app.state().zip_enabled; @@ -288,6 +292,7 @@ fn configure_app(app: App<MiniserveConfig>) -> App<MiniserveConfig> { file_upload, random_route.clone(), default_color_scheme, + show_qrcode, u_r.clone(), tar_enabled, zip_enabled, diff --git a/src/renderer.rs b/src/renderer.rs index accb49b..7270e8f 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -19,6 +19,7 @@ pub fn page( sort_order: Option<SortingOrder>, default_color_scheme: ColorScheme, color_scheme: ColorScheme, + show_qrcode: bool, file_upload: bool, upload_route: &str, current_dir: &str, @@ -46,7 +47,7 @@ pub fn page( } } } - (color_scheme_selector(sort_method, sort_order, color_scheme, default_color_scheme, serve_path)) + (color_scheme_selector(sort_method, sort_order, color_scheme, default_color_scheme, serve_path, show_qrcode)) div.container { span#top { } h1.title { "Index of " (serve_path) } @@ -133,24 +134,33 @@ fn color_scheme_selector( active_color_scheme: ColorScheme, default_color_scheme: ColorScheme, serve_path: &str, + show_qrcode: bool, ) -> Markup { html! { nav { - ul { - li { - a.change-theme href="#" title="Change theme" { - "Change theme..." + @if show_qrcode { + div { + p onmouseover="document.querySelector('#qrcode').src = `/?qrcode=${encodeURIComponent(window.location.href)}`" { + "QR code" } - ul { - @for color_scheme in ColorScheme::iter() { - @if active_color_scheme == color_scheme { - li.active { - (color_scheme_link(sort_method, sort_order, color_scheme, default_color_scheme, serve_path)) - } - } @else { - li { - (color_scheme_link(sort_method, sort_order, color_scheme, default_color_scheme, serve_path)) - } + div.qrcode { + img#qrcode alt="QR code" title="QR code of this page"; + } + } + } + div { + p { + "Change theme..." + } + ul.theme { + @for color_scheme in ColorScheme::iter() { + @if active_color_scheme == color_scheme { + li.active { + (color_scheme_link(sort_method, sort_order, color_scheme, default_color_scheme, serve_path)) + } + } @else { + li { + (color_scheme_link(sort_method, sort_order, color_scheme, default_color_scheme, serve_path)) } } } @@ -429,64 +439,75 @@ fn css(color_scheme: ColorScheme) -> Markup { }} nav {{ padding: 0 5rem; + display: flex; + justify-content: flex-end; }} - nav ul {{ - text-align: right; - list-style: none; - margin: 0; - padding: 0; - }} - nav ul li {{ - display: block; - transition-duration: 0.5s; - float: right; + nav > div {{ position: relative; + margin-left: 0.5rem; + }} + nav p {{ padding: 0.5rem 1rem; - background: {switch_theme_background}; width: 8rem; text-align: center; + background: {switch_theme_background}; + color: {change_theme_link_color}; + }} + nav p + * {{ + display: none; + position: absolute; + left: 0; + right: 0; + top: 100%; + animation: show 0.5s ease; + }} + @keyframes show {{ + from {{ + opacity: 0; + }} + to {{ + opacity: 1; + }} }} - nav ul li:hover {{ + nav > div::hover p {{ cursor: pointer; - text-decoration: none; - color: {change_theme_link_color} + color: {switch_theme_link_color}; }} - nav ul li a:hover {{ - text-decoration: none; - color: {change_theme_link_color_hover}; + nav > div:hover p + * {{ + display: block; + border-top: 1px solid {switch_theme_border}; }} - nav ul li ul {{ - visibility: hidden; - opacity: 0; - position: absolute; - transition: all 0.5s ease; - margin-top: 0.5rem; - left: 0; - display: none; - text-align: center; + nav .qrcode {{ + padding: 0.5rem; + background: {switch_theme_background}; }} - nav ul li:hover > ul, - nav ul li ul:hover {{ - visibility: visible; - opacity: 1; + nav .qrcode img {{ display: block; }} - nav ul li ul li:first-of-type {{ - border-top: 1px solid {switch_theme_border}; + nav .theme {{ + margin: 0; + padding: 0; + list-style-type: none; }} - nav ul li ul li {{ - clear: both; - width: 8rem; - padding-top: 0.5rem; - padding-bottom: 0.5rem; + nav .theme li {{ + width: 100%; + background: {switch_theme_background}; }} - nav ul li ul li a:hover {{ - text-decoration: underline; + nav .theme li a {{ + display: block; + width: 100%; + padding: 0.5rem 0; + text-align: center; + color: {switch_theme_link_color}; }} - nav ul li a, nav ul li ul li a, nav ul li a:visited, nav ul li ul li a:visited {{ - color: {switch_theme_link_color} + nav .theme li a:visited {{ + color: {switch_theme_link_color}; + }} + nav .theme li a::hover {{ + text-decoration: underline; + color: {change_theme_link_color_hover}; }} - nav ul li ul li.active a {{ + nav .theme li.active a {{ font-weight: bold; color: {switch_theme_active}; }} diff --git a/tests/qrcode.rs b/tests/qrcode.rs new file mode 100644 index 0000000..b032267 --- /dev/null +++ b/tests/qrcode.rs @@ -0,0 +1,88 @@ +mod fixtures; + +use assert_cmd::prelude::*; +use assert_fs::fixture::TempDir; +use fixtures::{port, tmpdir, Error}; +use reqwest::StatusCode; +use rstest::rstest; +use select::document::Document; +use select::predicate::Attr; +use std::iter::repeat_with; +use std::process::{Command, Stdio}; +use std::thread::sleep; +use std::time::Duration; + +#[rstest] +fn hide_qrcode_element(tmpdir: TempDir, port: u16) -> Result<(), Error> { + let mut child = Command::cargo_bin("miniserve")? + .arg(tmpdir.path()) + .arg("-p") + .arg(port.to_string()) + .stdout(Stdio::null()) + .spawn()?; + + sleep(Duration::from_secs(1)); + + let body = reqwest::blocking::get(format!("http://localhost:{}", port).as_str())? + .error_for_status()?; + let parsed = Document::from_read(body)?; + assert!(parsed.find(Attr("id", "qrcode")).next().is_none()); + + child.kill()?; + + Ok(()) +} + +#[rstest] +fn show_qrcode_element(tmpdir: TempDir, port: u16) -> Result<(), Error> { + let mut child = Command::cargo_bin("miniserve")? + .arg(tmpdir.path()) + .arg("-p") + .arg(port.to_string()) + .arg("-q") + .stdout(Stdio::null()) + .spawn()?; + + sleep(Duration::from_secs(1)); + + let body = reqwest::blocking::get(format!("http://localhost:{}", port).as_str())? + .error_for_status()?; + let parsed = Document::from_read(body)?; + assert!(parsed.find(Attr("id", "qrcode")).next().is_some()); + + child.kill()?; + + Ok(()) +} + +#[rstest] +fn get_svg_qrcode(tmpdir: TempDir, port: u16) -> Result<(), Error> { + + let mut child = Command::cargo_bin("miniserve")? + .arg(tmpdir.path()) + .arg("-p") + .arg(port.to_string()) + .stdout(Stdio::null()) + .spawn()?; + + sleep(Duration::from_secs(1)); + + // Ok + let resp = reqwest::blocking::get(format!("http://localhost:{}/?qrcode=test", port).as_str())?; + + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!(resp.headers()["Content-Type"], "image/svg+xml"); + let body = resp.text()?; + assert!(body.starts_with("<?xml")); + assert_eq!(body.len(), 3530); + + // Err + let content: String = repeat_with(|| '0').take(8 * 1024).collect(); + let resp = reqwest::blocking::get(format!("http://localhost:{}/?qrcode={}", port, content).as_str())?; + + assert_eq!(resp.status(), StatusCode::URI_TOO_LONG); + + child.kill()?; + + Ok(()) +} |