aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock7
-rw-r--r--Cargo.toml1
-rw-r--r--README.md1
-rw-r--r--src/args.rs5
-rw-r--r--src/listing.rs23
-rw-r--r--src/main.rs5
-rw-r--r--src/renderer.rs135
-rw-r--r--tests/qrcode.rs88
8 files changed, 208 insertions, 57 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 96fe95f..c57185a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/Cargo.toml b/Cargo.toml
index 7d687f3..6383e22 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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"
diff --git a/README.md b/README.md
index 36f9da0..3dee618 100644
--- a/README.md
+++ b/README.md
@@ -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,
&current_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(())
+}