diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/archive.rs | 31 | ||||
-rw-r--r-- | src/args.rs | 121 | ||||
-rw-r--r-- | src/errors.rs | 52 | ||||
-rw-r--r-- | src/file_upload.rs | 4 | ||||
-rw-r--r-- | src/listing.rs | 30 | ||||
-rw-r--r-- | src/main.rs | 132 | ||||
-rw-r--r-- | src/renderer.rs | 13 |
7 files changed, 221 insertions, 162 deletions
diff --git a/src/archive.rs b/src/archive.rs index 894ee3f..e53aea8 100644 --- a/src/archive.rs +++ b/src/archive.rs @@ -53,9 +53,9 @@ impl CompressionMethod { } } - pub fn is_enabled(self, tar_enabled: bool, zip_enabled: bool) -> bool { + pub fn is_enabled(self, tar_enabled: bool, tar_gz_enabled: bool, zip_enabled: bool) -> bool { match self { - CompressionMethod::TarGz => tar_enabled, + CompressionMethod::TarGz => tar_gz_enabled, CompressionMethod::Tar => tar_enabled, CompressionMethod::Zip => zip_enabled, } @@ -220,15 +220,18 @@ where let mut buffer = Vec::new(); while !paths_queue.is_empty() { let next = paths_queue.pop().ok_or_else(|| { - ContextualError::CustomError("Could not get path from queue".to_string()) + ContextualError::ArchiveCreationDetailError("Could not get path from queue".to_string()) })?; let current_dir = next.as_path(); let directory_entry_iterator = std::fs::read_dir(current_dir) .map_err(|e| ContextualError::IoError("Could not read directory".to_string(), e))?; - let zip_directory = - Path::new(zip_root_folder_name).join(current_dir.strip_prefix(directory).map_err( - |_| ContextualError::CustomError("Could not append base directory".to_string()), - )?); + let zip_directory = Path::new(zip_root_folder_name).join( + current_dir.strip_prefix(directory).map_err(|_| { + ContextualError::ArchiveCreationDetailError( + "Could not append base directory".to_string(), + ) + })?, + ); for entry in directory_entry_iterator { let entry_path = entry @@ -259,10 +262,14 @@ where zip_writer .start_file(relative_path.to_string_lossy(), options) .map_err(|_| { - ContextualError::CustomError("Could not add file path to ZIP".to_string()) + ContextualError::ArchiveCreationDetailError( + "Could not add file path to ZIP".to_string(), + ) })?; zip_writer.write(buffer.as_ref()).map_err(|_| { - ContextualError::CustomError("Could not write file to ZIP".to_string()) + ContextualError::ArchiveCreationDetailError( + "Could not write file to ZIP".to_string(), + ) })?; buffer.clear(); } else if entry_metadata.is_dir() { @@ -270,7 +277,7 @@ where zip_writer .add_directory(relative_path.to_string_lossy(), options) .map_err(|_| { - ContextualError::CustomError( + ContextualError::ArchiveCreationDetailError( "Could not add directory path to ZIP".to_string(), ) })?; @@ -280,7 +287,9 @@ where } zip_writer.finish().map_err(|_| { - ContextualError::CustomError("Could not finish writing ZIP archive".to_string()) + ContextualError::ArchiveCreationDetailError( + "Could not finish writing ZIP archive".to_string(), + ) })?; Ok(()) } diff --git a/src/args.rs b/src/args.rs index 909a88f..819618f 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1,7 +1,6 @@ use bytes::Bytes; use http::header::{HeaderMap, HeaderName, HeaderValue}; -use port_check::free_local_port; -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use std::net::IpAddr; use std::path::PathBuf; use structopt::StructOpt; @@ -9,11 +8,6 @@ use crate::auth; use crate::errors::ContextualError; use crate::renderer; -/// Possible characters for random routes -const ROUTE_ALPHABET: [char; 16] = [ - '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'a', 'b', 'c', 'd', 'e', 'f', -]; - #[derive(StructOpt)] #[structopt( name = "miniserve", @@ -21,25 +15,25 @@ const ROUTE_ALPHABET: [char; 16] = [ about, global_settings = &[structopt::clap::AppSettings::ColoredHelp], )] -struct CliArgs { +pub struct CliArgs { /// Be verbose, includes emitting access logs #[structopt(short = "v", long = "verbose")] - verbose: bool, + pub verbose: bool, /// Which path to serve #[structopt(name = "PATH", parse(from_os_str))] - path: Option<PathBuf>, + pub path: Option<PathBuf>, /// The name of a directory index file to serve, like "index.html" /// /// Normally, when miniserve serves a directory, it creates a listing for that directory. /// However, if a directory contains this file, miniserve will serve that file instead. #[structopt(long, parse(from_os_str), name = "index_file")] - index: Option<PathBuf>, + pub index: Option<PathBuf>, /// Port to use #[structopt(short = "p", long = "port", default_value = "8080")] - port: u16, + pub port: u16, /// Interface to listen on #[structopt( @@ -48,7 +42,7 @@ struct CliArgs { parse(try_from_str = parse_interface), number_of_values = 1, )] - interfaces: Vec<IpAddr>, + pub interfaces: Vec<IpAddr>, /// Set authentication. Currently supported formats: /// username:password, username:sha256:hash, username:sha512:hash @@ -59,19 +53,19 @@ struct CliArgs { parse(try_from_str = parse_auth), number_of_values = 1, )] - auth: Vec<auth::RequiredAuth>, + pub auth: Vec<auth::RequiredAuth>, /// Generate a random 6-hexdigit route #[structopt(long = "random-route")] - random_route: bool, + pub random_route: bool, /// Do not follow symbolic links #[structopt(short = "P", long = "no-symlinks")] - no_symlinks: bool, + pub no_symlinks: bool, /// Show hidden files #[structopt(short = "H", long = "hidden")] - hidden: bool, + pub hidden: bool, /// Default color scheme #[structopt( @@ -81,7 +75,7 @@ struct CliArgs { possible_values = &renderer::THEME_SLUGS, case_insensitive = true, )] - color_scheme: String, + pub color_scheme: String, /// Default color scheme #[structopt( @@ -91,46 +85,54 @@ struct CliArgs { possible_values = &renderer::THEME_SLUGS, case_insensitive = true, )] - color_scheme_dark: String, + pub color_scheme_dark: String, /// Enable QR code display #[structopt(short = "q", long = "qrcode")] - qrcode: bool, + pub qrcode: bool, /// Enable file uploading #[structopt(short = "u", long = "upload-files")] - file_upload: bool, + pub file_upload: bool, /// Enable overriding existing files during file upload #[structopt(short = "o", long = "overwrite-files")] - overwrite_files: bool, + pub overwrite_files: bool, - /// Enable tar archive generation + /// Enable uncompressed tar archive generation #[structopt(short = "r", long = "enable-tar")] - enable_tar: bool, + pub enable_tar: bool, + + /// Enable gz-compressed tar archive generation + #[structopt(short = "g", long = "enable-tar-gz")] + pub enable_tar_gz: bool, /// Enable zip archive generation /// /// WARNING: Zipping large directories can result in out-of-memory exception /// because zip generation is done in memory and cannot be sent on the fly #[structopt(short = "z", long = "enable-zip")] - enable_zip: bool, + pub enable_zip: bool, /// List directories first #[structopt(short = "D", long = "dirs-first")] - dirs_first: bool, + pub dirs_first: bool, /// Shown instead of host in page title and heading #[structopt(short = "t", long = "title")] - title: Option<String>, + pub title: Option<String>, /// Set custom header for responses #[structopt(long = "header", parse(try_from_str = parse_header), number_of_values = 1)] - header: Vec<HeaderMap>, + pub header: Vec<HeaderMap>, /// Hide version footer #[structopt(short = "F", long = "hide-version-footer")] - hide_version_footer: bool, + pub hide_version_footer: bool, + + /// Generate completion file for a shell + #[structopt(long = "print-completions", value_name = "shell", possible_values = &structopt::clap::Shell::variants())] + pub print_completions: Option<structopt::clap::Shell>, } /// Checks wether an interface is valid, i.e. it can be parsed into an IP address @@ -204,67 +206,6 @@ pub fn parse_header(src: &str) -> Result<HeaderMap, httparse::Error> { Ok(header_map) } -/// Parses the command line arguments -pub fn parse_args() -> crate::MiniserveConfig { - let args = CliArgs::from_args(); - - let interfaces = if !args.interfaces.is_empty() { - args.interfaces - } else { - vec![ - IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)), - IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), - ] - }; - - let random_route = if args.random_route { - Some(nanoid::nanoid!(6, &ROUTE_ALPHABET)) - } else { - None - }; - - // Generate some random routes for the favicon and css so that they are very unlikely to conflict with - // real files. - let favicon_route = nanoid::nanoid!(10, &ROUTE_ALPHABET); - let css_route = nanoid::nanoid!(10, &ROUTE_ALPHABET); - - let default_color_scheme = args.color_scheme; - let default_color_scheme_dark = args.color_scheme_dark; - - let path_explicitly_chosen = args.path.is_some() || args.index.is_some(); - - let port = match args.port { - 0 => free_local_port().expect("no free ports available"), - _ => args.port, - }; - - crate::MiniserveConfig { - verbose: args.verbose, - path: args.path.unwrap_or_else(|| PathBuf::from(".")), - port, - interfaces, - auth: args.auth, - path_explicitly_chosen, - no_symlinks: args.no_symlinks, - show_hidden: args.hidden, - random_route, - favicon_route, - css_route, - default_color_scheme, - default_color_scheme_dark, - 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, - dirs_first: args.dirs_first, - title: args.title, - header: args.header, - hide_version_footer: args.hide_version_footer, - } -} - #[rustfmt::skip] #[cfg(test)] mod tests { diff --git a/src/errors.rs b/src/errors.rs index 3287fc3..f079657 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -2,65 +2,78 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum ContextualError { - /// Fully customized errors, not inheriting from any error - #[error("{0}")] - CustomError(String), - /// Any kind of IO errors #[error("{0}\ncaused by: {1}")] IoError(String, std::io::Error), - /// MultipartError, which might occur during file upload, when processing the multipart request fails + /// Might occur during file upload, when processing the multipart request fails #[error("Failed to process multipart request\ncaused by: {0}")] MultipartError(actix_multipart::MultipartError), + /// Might occur during file upload + #[error("File already exists, and the overwrite_files option has not been set")] + DuplicateFileError, + /// Any error related to an invalid path (failed to retrieve entry name, unexpected entry type, etc) #[error("Invalid path\ncaused by: {0}")] InvalidPathError(String), - /// This error might occur if the HTTP credential string does not respect the expected format + /// Might occur if the HTTP credential string does not respect the expected format #[error("Invalid format for credentials string. Expected username:password, username:sha256:hash or username:sha512:hash")] InvalidAuthFormat, - /// This error might occure if the hash method is neither sha256 nor sha512 + /// Might occure if the hash method is neither sha256 nor sha512 #[error("{0} is not a valid hashing method. Expected sha256 or sha512")] InvalidHashMethod(String), - /// This error might occur if the HTTP auth hash password is not a valid hex code + /// Might occur if the HTTP auth hash password is not a valid hex code #[error("Invalid format for password hash. Expected hex code")] InvalidPasswordHash, - /// This error might occur if the HTTP auth password exceeds 255 characters + /// Might occur if the HTTP auth password exceeds 255 characters #[error("HTTP password length exceeds 255 characters")] PasswordTooLongError, - /// This error might occur if the user has unsufficient permissions to create an entry in a given directory + /// Might occur if the user has unsufficient permissions to create an entry in a given directory #[error("Insufficient permissions to create file in {0}")] InsufficientPermissionsError(String), - /// Any error related to parsing. + /// Any error related to parsing #[error("Failed to parse {0}\ncaused by: {1}")] ParseError(String, String), - /// This error might occur when the creation of an archive fails + /// Might occur when the creation of an archive fails #[error("An error occured while creating the {0}\ncaused by: {1}")] ArchiveCreationError(String, Box<ContextualError>), - /// This error might occur when the HTTP authentication fails + /// More specific archive creation failure reason + #[error("{0}")] + ArchiveCreationDetailError(String), + + /// Might occur when the HTTP authentication fails #[error("An error occured during HTTP authentication\ncaused by: {0}")] HttpAuthenticationError(Box<ContextualError>), - /// This error might occur when the HTTP credentials are not correct + /// Might occur when the HTTP credentials are not correct #[error("Invalid credentials for HTTP authentication")] InvalidHttpCredentials, - /// This error might occur when an HTTP request is invalid + /// Might occur when an HTTP request is invalid #[error("Invalid HTTP request\ncaused by: {0}")] InvalidHttpRequestError(String), - /// This error might occur when trying to access a page that does not exist + /// Might occur when trying to access a page that does not exist #[error("Route {0} could not be found")] RouteNotFoundError(String), + + /// In case miniserve was invoked without an interactive terminal and without an explicit path + #[error("Refusing to start as no explicit serve path was set and no interactive terminal was attached +Please set an explicit serve path like: `miniserve /my/path`")] + NoExplicitPathAndNoTerminal, + + /// In case miniserve was invoked with --no-symlinks but the serve path is a symlink + #[error("The -P|--no-symlinks option was provided but the serve path '{0}' is a symlink")] + NoSymlinksOptionWithSymlinkServePath(String), } pub fn log_error_chain(description: String) { @@ -68,10 +81,3 @@ pub fn log_error_chain(description: String) { log::error!("{}", cause); } } - -/// This makes creating CustomErrors easier -impl From<String> for ContextualError { - fn from(msg: String) -> ContextualError { - ContextualError::CustomError(msg) - } -} diff --git a/src/file_upload.rs b/src/file_upload.rs index 785d72f..93b7109 100644 --- a/src/file_upload.rs +++ b/src/file_upload.rs @@ -20,9 +20,7 @@ fn save_file( overwrite_files: bool, ) -> Pin<Box<dyn Future<Output = Result<i64, ContextualError>>>> { if !overwrite_files && file_path.exists() { - return Box::pin(future::err(ContextualError::CustomError( - "File already exists, and the overwrite_files option has not been set".to_string(), - ))); + return Box::pin(future::err(ContextualError::DuplicateFileError)); } let mut file = match std::fs::File::create(&file_path) { diff --git a/src/listing.rs b/src/listing.rs index 9d74906..79a4172 100644 --- a/src/listing.rs +++ b/src/listing.rs @@ -4,7 +4,7 @@ use actix_web::http::StatusCode; use actix_web::web::Query; use actix_web::{HttpRequest, HttpResponse, Result}; use bytesize::ByteSize; -use percent_encoding::{percent_decode_str, utf8_percent_encode, AsciiSet, CONTROLS}; +use percent_encoding::{percent_decode_str, utf8_percent_encode}; use qrcodegen::{QrCode, QrCodeEcc}; use serde::Deserialize; use std::io; @@ -15,8 +15,17 @@ use strum_macros::{Display, EnumString}; use crate::archive::CompressionMethod; use crate::errors::{self, ContextualError}; use crate::renderer; - -const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`'); +use percent_encode_sets::PATH_SEGMENT; + +/// "percent-encode sets" as defined by WHATWG specs: +/// https://url.spec.whatwg.org/#percent-encoded-bytes +mod percent_encode_sets { + use percent_encoding::{AsciiSet, CONTROLS}; + const BASE: &AsciiSet = &CONTROLS.add(b'%'); + pub const QUERY: &AsciiSet = &BASE.add(b' ').add(b'"').add(b'#').add(b'<').add(b'>'); + pub const PATH: &AsciiSet = &QUERY.add(b'?').add(b'`').add(b'{').add(b'}'); + pub const PATH_SEGMENT: &AsciiSet = &PATH.add(b'/'); +} /// Query parameters #[derive(Deserialize)] @@ -155,6 +164,7 @@ pub fn directory_listing( show_qrcode: bool, upload_route: String, tar_enabled: bool, + tar_gz_enabled: bool, zip_enabled: bool, dirs_first: bool, hide_version_footer: bool, @@ -210,7 +220,7 @@ pub fn directory_listing( Component::Normal(s) => { name = s.to_string_lossy().to_string(); link_accumulator - .push_str(&(utf8_percent_encode(&name, FRAGMENT).to_string() + "/")); + .push_str(&(utf8_percent_encode(&name, PATH_SEGMENT).to_string() + "/")); } _ => name = "".to_string(), }; @@ -248,12 +258,7 @@ pub fn directory_listing( for entry in dir.path.read_dir()? { if dir.is_visible(&entry) || show_hidden { let entry = entry?; - let p = match entry.path().strip_prefix(&dir.path) { - Ok(p) => base.join(p), - Err(_) => continue, - }; // show file url as relative to static path - let file_url = utf8_percent_encode(&p.to_string_lossy(), FRAGMENT).to_string(); let file_name = entry.file_name().to_string_lossy().to_string(); let (is_symlink, metadata) = match entry.metadata() { Ok(metadata) if metadata.file_type().is_symlink() => { @@ -262,6 +267,10 @@ pub fn directory_listing( } res => (false, res), }; + let file_url = base + .join(&utf8_percent_encode(&file_name, PATH_SEGMENT).to_string()) + .to_string_lossy() + .to_string(); // if file is a directory, add '/' to the end of the name if let Ok(metadata) = metadata { @@ -328,7 +337,7 @@ pub fn directory_listing( } if let Some(compression_method) = query_params.download { - if !compression_method.is_enabled(tar_enabled, zip_enabled) { + if !compression_method.is_enabled(tar_enabled, tar_gz_enabled, zip_enabled) { return Ok(ServiceResponse::new( req.clone(), HttpResponse::Forbidden() @@ -411,6 +420,7 @@ pub fn directory_listing( &encoded_dir, breadcrumbs, tar_enabled, + tar_gz_enabled, zip_enabled, hide_version_footer, ) diff --git a/src/main.rs b/src/main.rs index 17ab204..f174d57 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,9 @@ +use std::io; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::thread; +use std::time::Duration; +use std::{io::Write, path::PathBuf}; + use actix_web::web; use actix_web::{ http::{header::ContentType, StatusCode}, @@ -6,11 +12,9 @@ use actix_web::{ use actix_web::{middleware, App, HttpRequest, HttpResponse}; use actix_web_httpauth::middleware::HttpAuthentication; use http::header::HeaderMap; -use std::io::{self, Write}; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; -use std::thread; -use std::time::Duration; +use log::{error, warn}; use structopt::clap::crate_version; +use structopt::StructOpt; use yansi::{Color, Paint}; mod archive; @@ -24,6 +28,11 @@ mod renderer; use crate::errors::ContextualError; +/// Possible characters for random routes +const ROUTE_ALPHABET: [char; 16] = [ + '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'a', 'b', 'c', 'd', 'e', 'f', +]; + #[derive(Clone)] /// Configuration of the Miniserve application pub struct MiniserveConfig { @@ -81,9 +90,12 @@ pub struct MiniserveConfig { /// Enable upload to override existing files pub overwrite_files: bool, - /// If false, creation of tar archives is disabled + /// If false, creation of uncompressed tar archives is disabled pub tar_enabled: bool, + /// If false, creation of gz-compressed tar archives is disabled + pub tar_gz_enabled: bool, + /// If false, creation of zip archives is disabled pub zip_enabled: bool, @@ -100,31 +112,101 @@ pub struct MiniserveConfig { pub hide_version_footer: bool, } +impl MiniserveConfig { + /// Parses the command line arguments + fn from_args(args: args::CliArgs) -> Self { + let interfaces = if !args.interfaces.is_empty() { + args.interfaces + } else { + vec![ + IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)), + IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), + ] + }; + + let random_route = if args.random_route { + Some(nanoid::nanoid!(6, &ROUTE_ALPHABET)) + } else { + None + }; + + // Generate some random routes for the favicon and css so that they are very unlikely to conflict with + // real files. + let favicon_route = nanoid::nanoid!(10, &ROUTE_ALPHABET); + let css_route = nanoid::nanoid!(10, &ROUTE_ALPHABET); + + let default_color_scheme = args.color_scheme; + let default_color_scheme_dark = args.color_scheme_dark; + + let path_explicitly_chosen = args.path.is_some() || args.index.is_some(); + + let port = match args.port { + 0 => port_check::free_local_port().expect("no free ports available"), + _ => args.port, + }; + + crate::MiniserveConfig { + verbose: args.verbose, + path: args.path.unwrap_or_else(|| PathBuf::from(".")), + port, + interfaces, + auth: args.auth, + path_explicitly_chosen, + no_symlinks: args.no_symlinks, + show_hidden: args.hidden, + random_route, + favicon_route, + css_route, + default_color_scheme, + default_color_scheme_dark, + index: args.index, + overwrite_files: args.overwrite_files, + show_qrcode: args.qrcode, + file_upload: args.file_upload, + tar_enabled: args.enable_tar, + tar_gz_enabled: args.enable_tar_gz, + zip_enabled: args.enable_zip, + dirs_first: args.dirs_first, + title: args.title, + header: args.header, + hide_version_footer: args.hide_version_footer, + } + } +} + fn main() { - match run() { + let args = args::CliArgs::from_args(); + + if let Some(shell) = args.print_completions { + args::CliArgs::clap().gen_completions_to("miniserve", shell, &mut std::io::stdout()); + return; + } + + let miniserve_config = MiniserveConfig::from_args(args); + + match run(miniserve_config) { Ok(()) => (), Err(e) => errors::log_error_chain(e.to_string()), } } #[actix_web::main(miniserve)] -async fn run() -> Result<(), ContextualError> { +async fn run(miniserve_config: MiniserveConfig) -> Result<(), ContextualError> { if cfg!(windows) && !Paint::enable_windows_ascii() { Paint::disable(); } - let miniserve_config = args::parse_args(); - let log_level = if miniserve_config.verbose { simplelog::LevelFilter::Info } else { - simplelog::LevelFilter::Error + simplelog::LevelFilter::Warn }; if simplelog::TermLogger::init( log_level, simplelog::Config::default(), simplelog::TerminalMode::Mixed, + simplelog::ColorChoice::Auto, ) .is_err() { @@ -143,8 +225,8 @@ async fn run() -> Result<(), ContextualError> { .is_symlink(); if is_symlink { - return Err(ContextualError::from( - "The no-symlinks option cannot be used with a symlink path".to_string(), + return Err(ContextualError::NoSymlinksOptionWithSymlinkServePath( + miniserve_config.path.to_string_lossy().to_string(), )); } } @@ -175,9 +257,9 @@ async fn run() -> Result<(), ContextualError> { if let Some(index_path) = &miniserve_config.index { let has_index: std::path::PathBuf = [&canon_path, index_path].iter().collect(); if !has_index.exists() { - println!( - "{warning} The provided index file could not be found.", - warning = Color::RGB(255, 192, 0).paint("Notice:").bold() + error!( + "The file '{}' provided for option --index could not be found.", + index_path.to_string_lossy() ); } } @@ -189,9 +271,17 @@ async fn run() -> Result<(), ContextualError> { version = crate_version!() ); if !miniserve_config.path_explicitly_chosen { - println!("{warning} miniserve has been invoked without an explicit path so it will serve the current directory.", warning=Color::RGB(255, 192, 0).paint("Notice:").bold()); - println!( - " Invoke with -h|--help to see options or invoke as `miniserve .` to hide this advice." + // If the path to serve has NOT been explicitly chosen and if this is NOT an interactive + // terminal, we should refuse to start for security reasons. This would be the case when + // running miniserve as a service but forgetting to set the path. This could be pretty + // dangerous if given with an undesired context path (for instance /root or /). + if !atty::is(atty::Stream::Stdout) { + return Err(ContextualError::NoExplicitPathAndNoTerminal); + } + + warn!("miniserve has been invoked without an explicit path so it will serve the current directory after a short delay."); + warn!( + "Invoke with -h|--help to see options or invoke as `miniserve .` to hide this advice." ); print!("Starting server in "); io::stdout() @@ -284,7 +374,9 @@ async fn run() -> Result<(), ContextualError> { addresses = addresses, ); - println!("\nQuit by pressing CTRL-C"); + if atty::is(atty::Stream::Stdout) { + println!("\nQuit by pressing CTRL-C"); + } srv.await .map_err(|e| ContextualError::IoError("".to_owned(), e)) @@ -323,6 +415,7 @@ fn configure_app(app: &mut web::ServiceConfig, conf: &MiniserveConfig) { let show_qrcode = conf.show_qrcode; let file_upload = conf.file_upload; let tar_enabled = conf.tar_enabled; + let tar_gz_enabled = conf.tar_gz_enabled; let zip_enabled = conf.zip_enabled; let dirs_first = conf.dirs_first; let hide_version_footer = conf.hide_version_footer; @@ -365,6 +458,7 @@ fn configure_app(app: &mut web::ServiceConfig, conf: &MiniserveConfig) { show_qrcode, u_r.clone(), tar_enabled, + tar_gz_enabled, zip_enabled, dirs_first, hide_version_footer, diff --git a/src/renderer.rs b/src/renderer.rs index c51f364..1f57164 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -26,6 +26,7 @@ pub fn page( encoded_dir: &str, breadcrumbs: Vec<Breadcrumb>, tar_enabled: bool, + tar_gz_enabled: bool, zip_enabled: bool, hide_version_footer: bool, ) -> Markup { @@ -80,24 +81,24 @@ pub fn page( (color_scheme_selector(show_qrcode)) div.container { span#top { } - h1.title { + h1.title dir="ltr" { @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) } + span { bdi { (el.name) } } } @else { - a.directory href=(parametrized_link(&el.link, sort_method, sort_order)) { - (el.name) + a href=(parametrized_link(&el.link, sort_method, sort_order)) { + bdi { (el.name) } } } "/" } } div.toolbar { - @if tar_enabled || zip_enabled { + @if tar_enabled || tar_gz_enabled || zip_enabled { div.download { @for compression_method in CompressionMethod::iter() { - @if compression_method.is_enabled(tar_enabled, zip_enabled) { + @if compression_method.is_enabled(tar_enabled, tar_gz_enabled, zip_enabled) { (archive_button(compression_method, sort_method, sort_order)) } } |