Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add title option (#335) and breadcrumb links in heading #378

Merged
merged 5 commits into from
Sep 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ struct CLIArgs {
/// because zip generation is done in memory and cannot be sent on the fly
#[structopt(short = "z", long = "enable-zip")]
enable_zip: bool,

/// Shown instead of host in page title and heading
#[structopt(short = "t", long = "title")]
title: Option<String>,
}

/// Checks wether an interface is valid, i.e. it can be parsed into an IP address
Expand Down Expand Up @@ -201,6 +205,7 @@ pub fn parse_args() -> crate::MiniserveConfig {
file_upload: args.file_upload,
tar_enabled: args.enable_tar,
zip_enabled: args.enable_zip,
title: args.title,
}
}

Expand Down
65 changes: 55 additions & 10 deletions src/listing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use percent_encoding::{percent_decode_str, utf8_percent_encode, AsciiSet, CONTRO
use qrcodegen::{QrCode, QrCodeEcc};
use serde::Deserialize;
use std::io;
use std::path::{Path, PathBuf};
use std::path::{Component, Path, PathBuf};
use std::time::SystemTime;
use strum_macros::{Display, EnumString};

Expand Down Expand Up @@ -123,6 +123,21 @@ impl Entry {
}
}

/// One entry in the path to the listed directory
pub struct Breadcrumb {
/// Name of directory
pub name: String,

/// Link to get to directory, relative to listed directory
pub link: String,
}

impl Breadcrumb {
fn new(name: String, link: String) -> Self {
Breadcrumb { name, link }
}
}

pub async fn file_handler(req: HttpRequest) -> Result<actix_files::NamedFile> {
let path = &req.app_data::<crate::MiniserveConfig>().unwrap().path;
actix_files::NamedFile::open(path).map_err(Into::into)
Expand All @@ -143,6 +158,7 @@ pub fn directory_listing(
upload_route: String,
tar_enabled: bool,
zip_enabled: bool,
title: Option<String>,
) -> Result<ServiceResponse, io::Error> {
use actix_web::dev::BodyEncoding;
let serve_path = req.path();
Expand All @@ -163,23 +179,52 @@ pub fn directory_listing(
}

let base = Path::new(serve_path);
let random_route = format!("/{}", random_route.unwrap_or_default());
let is_root = base.parent().is_none() || Path::new(&req.path()) == Path::new(&random_route);
let random_route_abs = format!("/{}", random_route.clone().unwrap_or_default());
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) {
let encoded_dir = match base.strip_prefix(random_route_abs) {
Ok(c_d) => Path::new("/").join(c_d),
Err(_) => base.to_path_buf(),
}
.display()
.to_string();

let display_dir = {
let breadcrumbs = {
let title = title.unwrap_or_else(|| req.connection_info().host().into());

let decoded = percent_decode_str(&encoded_dir).decode_utf8_lossy();
if is_root {
decoded.to_string()
} else {
format!("{}/", decoded)

let mut res: Vec<Breadcrumb> = Vec::new();
let mut link_accumulator =
format!("/{}", random_route.map(|r| r + "/").unwrap_or_default());

let mut components = Path::new(&*decoded).components().peekable();

while let Some(c) = components.next() {
let name;

match c {
Component::RootDir => {
name = title.clone();
}
Component::Normal(s) => {
name = s.to_string_lossy().to_string();
link_accumulator
.push_str(&(utf8_percent_encode(&name, FRAGMENT).to_string() + "/"));
}
_ => unreachable!(),
};

res.push(Breadcrumb::new(
name,
if components.peek().is_some() {
link_accumulator.clone()
} else {
".".to_string()
},
));
}
res
};

let query_params = extract_query_parameters(req);
Expand Down Expand Up @@ -360,7 +405,7 @@ pub fn directory_listing(
&upload_route,
&favicon_route,
&encoded_dir,
&display_dir,
breadcrumbs,
tar_enabled,
zip_enabled,
)
Expand Down
5 changes: 5 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ pub struct MiniserveConfig {

/// If false, creation of zip archives is disabled
pub zip_enabled: bool,

/// Shown instead of host in page title and heading
pub title: Option<String>,
}

fn main() {
Expand Down Expand Up @@ -286,6 +289,7 @@ fn configure_app(app: &mut web::ServiceConfig, conf: &MiniserveConfig) {
let file_upload = conf.file_upload;
let tar_enabled = conf.tar_enabled;
let zip_enabled = conf.zip_enabled;
let title = conf.title.clone();
upload_route = if let Some(random_route) = conf.random_route.clone() {
format!("/{}/upload", random_route)
} else {
Expand Down Expand Up @@ -315,6 +319,7 @@ fn configure_app(app: &mut web::ServiceConfig, conf: &MiniserveConfig) {
u_r.clone(),
tar_enabled,
zip_enabled,
title.clone(),
)
})
.default_handler(web::to(error_404)),
Expand Down
40 changes: 27 additions & 13 deletions src/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::time::SystemTime;
use strum::IntoEnumIterator;

use crate::archive::CompressionMethod;
use crate::listing::{Entry, SortingMethod, SortingOrder};
use crate::listing::{Breadcrumb, Entry, SortingMethod, SortingOrder};
use crate::themes::ColorScheme;

/// Renders the file listing
Expand All @@ -24,7 +24,7 @@ pub fn page(
upload_route: &str,
favicon_route: &str,
encoded_dir: &str,
display_dir: &str,
breadcrumbs: Vec<Breadcrumb>,
tar_enabled: bool,
zip_enabled: bool,
) -> Markup {
Expand All @@ -37,10 +37,16 @@ pub fn page(
default_color_scheme,
);

let title_path = breadcrumbs
.iter()
.map(|el| el.name.clone())
.collect::<Vec<_>>()
.join("/");

html! {
(DOCTYPE)
html {
(page_header(display_dir, color_scheme, file_upload, favicon_route, false))
(page_header(&title_path, color_scheme, file_upload, favicon_route))
body#drop-container {
@if file_upload {
div.drag-form {
Expand All @@ -52,7 +58,19 @@ pub fn page(
(color_scheme_selector(sort_method, sort_order, color_scheme, default_color_scheme, serve_path, show_qrcode))
div.container {
span#top { }
h1.title { "Index of " (display_dir) }
h1.title {
@for el in breadcrumbs {
@if el.link == "." {
// wrapped in span so the text doesn't shift slightly when it turns into a link
span { (el.name) }
} @else {
a.directory href=(parametrized_link(&el.link, sort_method, sort_order, color_scheme, default_color_scheme)) {
(el.name)
}
}
"/"
}
}
div.toolbar {
@if tar_enabled || zip_enabled {
div.download {
Expand Down Expand Up @@ -446,7 +464,7 @@ fn css(color_scheme: ColorScheme) -> Markup {
}}
nav > div {{
position: relative;
margin-left: 0.5rem;
margin-left: 0.5rem;
}}
nav p {{
padding: 0.5rem 1rem;
Expand Down Expand Up @@ -836,23 +854,19 @@ fn chevron_down() -> Markup {

/// Partial: page header
fn page_header(
serve_path: &str,
title: &str,
color_scheme: ColorScheme,
file_upload: bool,
favicon_route: &str,
is_error: bool,
) -> Markup {
html! {
head {
meta charset="utf-8";
meta http-equiv="X-UA-Compatible" content="IE=edge";
meta name="viewport" content="width=device-width, initial-scale=1";
link rel="icon" type="image/svg+xml" href={ "/" (favicon_route) };
@if is_error {
title { (serve_path) }
} @else {
title { "Index of " (serve_path) }
}
title { (title) }

style { (css(color_scheme)) }
@if file_upload {
(PreEscaped(r#"
Expand Down Expand Up @@ -944,7 +958,7 @@ pub fn render_error(
html! {
(DOCTYPE)
html {
(page_header(&error_code.to_string(), color_scheme, false, favicon_route, true))
(page_header(&error_code.to_string(), color_scheme, false, favicon_route))
body {
div.error {
p { (error_code.to_string()) }
Expand Down
60 changes: 60 additions & 0 deletions tests/navigation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,63 @@ fn can_navigate_deep_into_dirs_and_back(tmpdir: TempDir, port: u16) -> Result<()

Ok(())
}

#[rstest(use_custom_title, case(true), case(false))]
/// We can use breadcrumbs to navigate.
fn can_navigate_using_breadcrumbs(
tmpdir: TempDir,
port: u16,
use_custom_title: bool,
) -> Result<(), Error> {
let mut command_base = Command::cargo_bin("miniserve")?;
let mut command = command_base.arg("-p").arg(port.to_string());

if use_custom_title {
command = command.arg("--title").arg("some title")
}

let mut child = command.arg(tmpdir.path()).stdout(Stdio::null()).spawn()?;

sleep(Duration::from_secs(1));

// Create a vector of directory names. We don't need to fetch the file and so we'll
// remove that part.
let dir: String = {
let mut comps = DEEPLY_NESTED_FILE
.split("/")
.map(|d| format!("{}/", d))
.collect::<Vec<String>>();
comps.pop();
comps.join("")
};

let base_url = Url::parse(&format!("http://localhost:{}/", port))?;
let nested_url = base_url.join(&dir)?;

let resp = reqwest::blocking::get(nested_url.as_str())?;
let body = resp.error_for_status()?;
let parsed = Document::from_read(body)?;

let title_name = if use_custom_title {
"some title".to_string()
} else {
format!("localhost:{}", port)
};

// can go back to root dir by clicking title
let title_link = get_link_from_text(&parsed, &title_name).expect("Root dir link not found.");
assert_eq!("/", title_link);

// can go to intermediate dir
let intermediate_dir_link =
get_link_from_text(&parsed, "very").expect("Intermediate dir link not found.");
assert_eq!("/very/", intermediate_dir_link);

// current dir is not linked
let current_dir_link = get_link_from_text(&parsed, "nested");
assert_eq!(None, current_dir_link);

child.kill()?;

Ok(())
}