aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorjikstra <jikstra@disroot.org>2021-04-25 15:48:09 +0000
committerjikstra <jikstra@disroot.org>2021-09-01 19:08:00 +0000
commit06db56e40820ec0067d18840648ee786163a3862 (patch)
tree638eb60a484ba0b17a8373703141e19812dd0859
parentAdd CHANGELOG entry for printing QR codes on terminal (diff)
downloadminiserve-06db56e40820ec0067d18840648ee786163a3862.tar.gz
miniserve-06db56e40820ec0067d18840648ee786163a3862.zip
Implement a raw rendering mode for recursive folder download
- Raw mode only displays file/folders and is more focused on computer processing - Display a banner in footer to recursively download the current folder with wget
Diffstat (limited to '')
-rw-r--r--data/style.scss14
-rw-r--r--src/auth.rs20
-rw-r--r--src/listing.rs7
-rw-r--r--src/renderer.rs118
-rw-r--r--tests/raw.rs103
5 files changed, 242 insertions, 20 deletions
diff --git a/data/style.scss b/data/style.scss
index 7e63751..5a05fe7 100644
--- a/data/style.scss
+++ b/data/style.scss
@@ -52,6 +52,20 @@ body {
padding-top: 1.5rem;
font-size: 0.7em;
color: var(--footer_color);
+
+ .downloadWget {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ flex-wrap: wrap;
+ .cmd {
+ margin: 0 0 auto;
+ padding-left: 5px;
+
+ line-height: 13px;
+ font-family: monospace;
+ }
+ }
}
a {
diff --git a/src/auth.rs b/src/auth.rs
index 0d97f11..43aca17 100644
--- a/src/auth.rs
+++ b/src/auth.rs
@@ -42,7 +42,7 @@ pub struct RequiredAuth {
}
/// Return `true` if `basic_auth` is matches any of `required_auth`
-pub fn match_auth(basic_auth: BasicAuthParams, required_auth: &[RequiredAuth]) -> bool {
+pub fn match_auth(basic_auth: &BasicAuthParams, required_auth: &[RequiredAuth]) -> bool {
required_auth
.iter()
.any(|RequiredAuth { username, password }| {
@@ -74,6 +74,9 @@ pub fn get_hash<T: Digest>(text: &str) -> Vec<u8> {
hasher.update(text);
hasher.finalize().to_vec()
}
+pub struct CurrentUser {
+ pub name: String,
+}
fn handle_auth(req: &HttpRequest) -> Result<(), ContextualError> {
let required_auth = &req.app_data::<crate::MiniserveConfig>().unwrap().auth;
@@ -85,7 +88,12 @@ fn handle_auth(req: &HttpRequest) -> Result<(), ContextualError> {
match BasicAuthParams::try_from_request(req) {
Ok(cred) => match match_auth(cred, required_auth) {
- true => Ok(()),
+ true => {
+ req.extensions_mut().insert(CurrentUser {
+ name: cred_params.username,
+ });
+ Ok(())
+ },
false => Err(ContextualError::InvalidHttpCredentials),
},
Err(_) => Err(ContextualError::RequireHttpCredentials),
@@ -173,7 +181,7 @@ mod tests {
) {
assert_eq!(
match_auth(
- BasicAuthParams {
+ &BasicAuthParams {
username: param_username.to_owned(),
password: param_password.to_owned(),
},
@@ -214,7 +222,7 @@ mod tests {
password: &str,
) {
assert!(match_auth(
- BasicAuthParams {
+ &BasicAuthParams {
username: username.to_owned(),
password: password.to_owned(),
},
@@ -225,7 +233,7 @@ mod tests {
#[rstest]
fn test_multiple_auth_wrong_username(account_sample: Vec<RequiredAuth>) {
assert_eq!(match_auth(
- BasicAuthParams {
+ &BasicAuthParams {
username: "unregistered user".to_owned(),
password: "pwd0".to_owned(),
},
@@ -248,7 +256,7 @@ mod tests {
password: &str,
) {
assert_eq!(match_auth(
- BasicAuthParams {
+ &BasicAuthParams {
username: username.to_owned(),
password: password.to_owned(),
},
diff --git a/src/listing.rs b/src/listing.rs
index b2730de..b00a671 100644
--- a/src/listing.rs
+++ b/src/listing.rs
@@ -12,6 +12,7 @@ use std::time::SystemTime;
use strum_macros::{Display, EnumString};
use crate::archive::ArchiveMethod;
+use crate::auth::CurrentUser;
use crate::errors::{self, ContextualError};
use crate::renderer;
use percent_encode_sets::PATH_SEGMENT;
@@ -32,6 +33,7 @@ pub struct QueryParameters {
pub path: Option<PathBuf>,
pub sort: Option<SortingMethod>,
pub order: Option<SortingOrder>,
+ pub raw: Option<bool>,
qrcode: Option<String>,
download: Option<ArchiveMethod>,
}
@@ -152,6 +154,9 @@ pub fn directory_listing(
dir: &actix_files::Directory,
req: &HttpRequest,
) -> io::Result<ServiceResponse> {
+ let extensions = req.extensions();
+ let current_user: Option<&CurrentUser> = extensions.get::<CurrentUser>();
+
use actix_web::dev::BodyEncoding;
let conf = req.app_data::<crate::MiniserveConfig>().unwrap();
let serve_path = req.path();
@@ -387,6 +392,7 @@ pub fn extract_query_parameters(req: &HttpRequest) -> QueryParameters {
sort: query.sort,
order: query.order,
download: query.download,
+ raw: query.raw,
qrcode: query.qrcode.to_owned(),
path: query.path.clone(),
},
@@ -397,6 +403,7 @@ pub fn extract_query_parameters(req: &HttpRequest) -> QueryParameters {
sort: None,
order: None,
download: None,
+ raw: None,
qrcode: None,
path: None,
}
diff --git a/src/renderer.rs b/src/renderer.rs
index d1821dd..ac6b640 100644
--- a/src/renderer.rs
+++ b/src/renderer.rs
@@ -17,6 +17,7 @@ pub fn page(
breadcrumbs: Vec<Breadcrumb>,
encoded_dir: &str,
conf: &MiniserveConfig,
+ current_user: Option<&CurrentUser>,
) -> Markup {
let upload_route = match conf.random_route {
Some(ref random_route) => format!("/{}/upload", random_route),
@@ -80,7 +81,7 @@ pub fn page(
// wrapped in span so the text doesn't shift slightly when it turns into a link
span { bdi { (el.name) } }
} @else {
- a href=(parametrized_link(&el.link, sort_method, sort_order)) {
+ a href=(parametrized_link(&el.link, sort_method, sort_order, false)) {
bdi { (el.name) }
}
}
@@ -120,22 +121,63 @@ pub fn page(
tr {
td colspan="3" {
span.root-chevron { (chevron_left()) }
- a.root href=(parametrized_link("../", sort_method, sort_order)) {
+ a.root href=(parametrized_link("../", sort_method, sort_order, false)) {
"Parent directory"
}
}
}
}
@for entry in entries {
- (entry_row(entry, sort_method, sort_order))
+ (entry_row(entry, sort_method, sort_order, false))
}
}
}
a.back href="#top" {
(arrow_up())
}
- @if !conf.hide_version_footer {
- (version_footer())
+ div.footer {
+ (wget_download(&title_path, current_user))
+ @if !conf.hide_version_footer {
+ (version_footer())
+ }
+ }
+ }
+ }
+ }
+ }
+}
+/// Renders the file listing
+#[allow(clippy::too_many_arguments)]
+pub fn raw(
+ entries: Vec<Entry>,
+ is_root: bool,
+ sort_method: Option<SortingMethod>,
+ sort_order: Option<SortingOrder>,
+) -> Markup {
+ html! {
+ (DOCTYPE)
+ html {
+ body {
+ table {
+ thead {
+ th.name { "Name" }
+ th.size { "Size" }
+ th.date { "Last modification" }
+ }
+ tbody {
+ @if !is_root {
+ tr {
+ td colspan="3" {
+ a.root href=(parametrized_link("../", sort_method, sort_order, true)) {
+ ".."
+ }
+ }
+ }
+ }
+ @for entry in entries {
+ (entry_row(entry, sort_method, sort_order, true))
+ }
+>>>>>>> 2949329 (Implement a raw rendering mode for recursive folder download)
}
}
}
@@ -146,12 +188,36 @@ pub fn page(
// Partial: version footer
fn version_footer() -> Markup {
html! {
- p.footer {
- (format!("{}/{}", crate_name!(), crate_version!()))
- }
+ div.version {
+ (format!("{}/{}", crate_name!(), crate_version!()))
+ }
}
}
+fn wget_download(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
+ }
+ };
+
+ let user_params = if let Some(user) = current_user {
+ format!(" --ask-password --user {}", user.name)
+ } else {
+ "".to_string()
+ };
+
+ return html! {
+ div.downloadWget {
+ p { "Download folder:" }
+ div.cmd { (format!("wget -r -c -nH -np --cut-dirs={} -R \"index.html*\"{} \"http://{}/?raw=true\"", count, user_params, title_path)) }
+ }
+ };
+}
+
/// Build the action of the upload form
fn build_upload_action(
upload_route: &str,
@@ -232,7 +298,7 @@ fn archive_button(
} else {
format!(
"{}&download={}",
- parametrized_link("", sort_method, sort_order,),
+ parametrized_link("", sort_method, sort_order, false),
archive_method
)
};
@@ -260,14 +326,19 @@ fn parametrized_link(
link: &str,
sort_method: Option<SortingMethod>,
sort_order: Option<SortingOrder>,
+ raw: bool,
) -> String {
+ if raw {
+ return format!("{}?raw=true", make_link_with_trailing_slash(link));
+ }
+
if let Some(method) = sort_method {
if let Some(order) = sort_order {
let parametrized_link = format!(
"{}?sort={}&order={}",
make_link_with_trailing_slash(link),
method,
- order
+ order,
);
return parametrized_link;
@@ -315,6 +386,7 @@ fn entry_row(
entry: Entry,
sort_method: Option<SortingMethod>,
sort_order: Option<SortingOrder>,
+ raw: bool,
) -> Markup {
html! {
tr {
@@ -322,7 +394,7 @@ fn entry_row(
p {
@if entry.is_dir() {
@if let Some(symlink_dest) = entry.symlink_info {
- a.symlink href=(parametrized_link(&entry.link, sort_method, sort_order)) {
+ a.symlink href=(parametrized_link(&entry.link, sort_method, sort_order, raw)) {
(entry.name) "/"
span.symlink-symbol { }
a.directory {(symlink_dest) "/"}
@@ -345,9 +417,11 @@ fn entry_row(
}
}
- @if let Some(size) = entry.size {
- span.mobile-info.size {
- (size)
+ @if !raw {
+ @if let Some(size) = entry.size {
+ span.mobile-info.size {
+ (size)
+ }
}
}
}
@@ -477,6 +551,15 @@ pub fn render_error(
conf: &MiniserveConfig,
return_address: &str,
) -> Markup {
+<<<<<<< HEAD
+=======
+ let link = if has_referer {
+ return_address.to_string()
+ } else {
+ parametrized_link(return_address, sort_method, sort_order, false)
+ };
+
+>>>>>>> 2949329 (Implement a raw rendering mode for recursive folder download)
html! {
(DOCTYPE)
html {
@@ -508,8 +591,15 @@ pub fn render_error(
}
}
}
+<<<<<<< HEAD
@if !conf.hide_version_footer {
(version_footer())
+=======
+ @if !hide_version_footer {
+ p.footer {
+ (version_footer())
+ }
+>>>>>>> 2949329 (Implement a raw rendering mode for recursive folder download)
}
}
}
diff --git a/tests/raw.rs b/tests/raw.rs
new file mode 100644
index 0000000..5c2227b
--- /dev/null
+++ b/tests/raw.rs
@@ -0,0 +1,103 @@
+mod fixtures;
+mod utils;
+
+use crate::fixtures::TestServer;
+use assert_cmd::prelude::*;
+use assert_fs::fixture::TempDir;
+use fixtures::{port, server, tmpdir, Error};
+use pretty_assertions::assert_eq;
+use reqwest::blocking::Client;
+use rstest::rstest;
+use select::document::Document;
+use select::predicate::Class;
+use select::predicate::Name;
+use std::process::{Command, Stdio};
+use std::thread::sleep;
+use std::time::Duration;
+
+#[rstest]
+/// The ui displays the correct wget command to download the folder recursively
+fn ui_displays_wget_element(server: TestServer) -> Result<(), Error> {
+ let client = Client::new();
+
+ let body = client.get(server.url()).send()?.error_for_status()?;
+ let parsed = Document::from_read(body)?;
+ let wget_url = parsed
+ .find(Class("downloadWget"))
+ .next()
+ .unwrap()
+ .find(Class("cmd"))
+ .next()
+ .unwrap()
+ .text();
+ assert_eq!(
+ wget_url,
+ format!(
+ "wget -r -c -nH -np --cut-dirs=0 -R \"index.html*\" \"{}?raw=true\"",
+ server.url()
+ )
+ );
+
+ let body = client
+ .get(format!("{}/very/deeply/nested/", server.url()))
+ .send()?
+ .error_for_status()?;
+ let parsed = Document::from_read(body)?;
+ let wget_url = parsed
+ .find(Class("downloadWget"))
+ .next()
+ .unwrap()
+ .find(Class("cmd"))
+ .next()
+ .unwrap()
+ .text();
+ assert_eq!(
+ wget_url,
+ format!(
+ "wget -r -c -nH -np --cut-dirs=2 -R \"index.html*\" \"{}very/deeply/nested/?raw=true\"",
+ server.url()
+ )
+ );
+
+ Ok(())
+}
+
+#[rstest]
+/// All hrefs in raw mode are links to directories or files & directories end with ?raw=true
+fn raw_mode_links_to_directories_end_with_raw_true(server: TestServer) -> Result<(), Error> {
+ fn verify_a_tags(parsed: Document) {
+ // Ensure all links end with ?raw=true or are files
+ for node in parsed.find(Name("a")) {
+ let class = node.attr("class").unwrap();
+
+ if class == "root" || class == "directory" {
+ assert!(node.attr("href").unwrap().ends_with("?raw=true"));
+ } else if class == "file" {
+ assert!(true);
+ } else {
+ println!(
+ "This node is a link and neither of class directory, root or file: {:?}",
+ node
+ );
+ assert!(false);
+ }
+ }
+ }
+
+ let urls = [
+ format!("{}?raw=true", server.url()),
+ format!("{}very?raw=true", server.url()),
+ format!("{}very/deeply/?raw=true", server.url()),
+ format!("{}very/deeply/nested?raw=true", server.url()),
+ ];
+
+ let client = Client::new();
+ // Ensure the links to the archives are not present
+ for url in urls.iter() {
+ let body = client.get(url).send()?.error_for_status()?;
+ let parsed = Document::from_read(body)?;
+ verify_a_tags(parsed);
+ }
+
+ Ok(())
+}