From a389db173b8b6f38c8330ddecf00023c72c8ee86 Mon Sep 17 00:00:00 2001 From: Andy Freeland Date: Thu, 25 Mar 2021 21:06:08 -0700 Subject: Generate completions with `miniserve --print-completions ` This patch adds a `--print-completions` option to generate shell completion files at runtime. This ensures the completions are always up to date. Fixes #377. --- src/args.rs | 10 ++++++---- src/main.rs | 16 ++++++++++++---- 2 files changed, 18 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/args.rs b/src/args.rs index 909a88f..d6cfc69 100644 --- a/src/args.rs +++ b/src/args.rs @@ -21,7 +21,7 @@ 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, @@ -131,6 +131,10 @@ struct CliArgs { /// Hide version footer #[structopt(short = "F", long = "hide-version-footer")] hide_version_footer: bool, + + /// Generate completion file for a shell + #[structopt(long = "print-completions", value_name = "shell")] + pub print_completions: Option, } /// Checks wether an interface is valid, i.e. it can be parsed into an IP address @@ -205,9 +209,7 @@ pub fn parse_header(src: &str) -> Result { } /// Parses the command line arguments -pub fn parse_args() -> crate::MiniserveConfig { - let args = CliArgs::from_args(); - +pub fn parse_args(args: CliArgs) -> crate::MiniserveConfig { let interfaces = if !args.interfaces.is_empty() { args.interfaces } else { diff --git a/src/main.rs b/src/main.rs index 17ab204..747a705 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,7 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::thread; use std::time::Duration; use structopt::clap::crate_version; +use structopt::StructOpt; use yansi::{Color, Paint}; mod archive; @@ -101,20 +102,27 @@ pub struct MiniserveConfig { } 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 = args::parse_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 { -- cgit v1.2.3 From 26b0a519d226f555674db481f3a3cd2fc52a9961 Mon Sep 17 00:00:00 2001 From: Sven-Hendrik Haase Date: Sun, 28 Mar 2021 21:16:39 +0200 Subject: Refactor and separate out arg handling and config handling --- src/args.rs | 107 ++++++++++++------------------------------------------------ src/main.rs | 78 +++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 94 insertions(+), 91 deletions(-) (limited to 'src') diff --git a/src/args.rs b/src/args.rs index d6cfc69..169b56f 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", @@ -24,22 +18,22 @@ const ROUTE_ALPHABET: [char; 16] = [ 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, + pub path: Option, /// 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, + pub index: Option, /// 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 @@ pub struct CliArgs { parse(try_from_str = parse_interface), number_of_values = 1, )] - interfaces: Vec, + pub interfaces: Vec, /// Set authentication. Currently supported formats: /// username:password, username:sha256:hash, username:sha512:hash @@ -59,19 +53,19 @@ pub struct CliArgs { parse(try_from_str = parse_auth), number_of_values = 1, )] - auth: Vec, + pub auth: Vec, /// 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 @@ pub struct CliArgs { possible_values = &renderer::THEME_SLUGS, case_insensitive = true, )] - color_scheme: String, + pub color_scheme: String, /// Default color scheme #[structopt( @@ -91,46 +85,46 @@ pub 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 #[structopt(short = "r", long = "enable-tar")] - enable_tar: bool, + pub enable_tar: 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, + pub title: Option, /// Set custom header for responses #[structopt(long = "header", parse(try_from_str = parse_header), number_of_values = 1)] - header: Vec, + pub header: Vec, /// 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")] @@ -208,65 +202,6 @@ pub fn parse_header(src: &str) -> Result { Ok(header_map) } -/// Parses the command line arguments -pub fn parse_args(args: CliArgs) -> crate::MiniserveConfig { - 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/main.rs b/src/main.rs index 747a705..a6410a6 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,10 +12,6 @@ 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 structopt::clap::crate_version; use structopt::StructOpt; use yansi::{Color, Paint}; @@ -25,6 +27,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 { @@ -101,6 +108,67 @@ 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, + 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() { let args = args::CliArgs::from_args(); @@ -109,7 +177,7 @@ fn main() { return; } - let miniserve_config = args::parse_args(args); + let miniserve_config = MiniserveConfig::from_args(args); match run(miniserve_config) { Ok(()) => (), -- cgit v1.2.3 From 531c9e0351035b23ae0c6088807358338c20b974 Mon Sep 17 00:00:00 2001 From: Sven-Hendrik Haase Date: Sun, 28 Mar 2021 21:19:27 +0200 Subject: Print supported shells for completions in help command --- src/args.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/args.rs b/src/args.rs index 169b56f..81e49dc 100644 --- a/src/args.rs +++ b/src/args.rs @@ -127,7 +127,7 @@ pub struct CliArgs { pub hide_version_footer: bool, /// Generate completion file for a shell - #[structopt(long = "print-completions", value_name = "shell")] + #[structopt(long = "print-completions", value_name = "shell", possible_values = &structopt::clap::Shell::variants())] pub print_completions: Option, } -- cgit v1.2.3 From dfe2b2460c33794ca91cf614cd4ead1382a1199a Mon Sep 17 00:00:00 2001 From: Sven-Hendrik Haase Date: Sun, 28 Mar 2021 21:28:20 +0200 Subject: Better message for when provided index file doesn't exist --- src/main.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/main.rs b/src/main.rs index a6410a6..8f29dc6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,7 @@ use actix_web::{ use actix_web::{middleware, App, HttpRequest, HttpResponse}; use actix_web_httpauth::middleware::HttpAuthentication; use http::header::HeaderMap; +use log::error; use structopt::clap::crate_version; use structopt::StructOpt; use yansi::{Color, Paint}; @@ -251,9 +252,9 @@ async fn run(miniserve_config: MiniserveConfig) -> 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() ); } } -- cgit v1.2.3 From 31b5b9faf3cacb68d89fb2509368e472636a9cb4 Mon Sep 17 00:00:00 2001 From: Sven-Hendrik Haase Date: Sun, 28 Mar 2021 21:30:29 +0200 Subject: Change default log level to Warn --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/main.rs b/src/main.rs index 8f29dc6..1b896da 100644 --- a/src/main.rs +++ b/src/main.rs @@ -195,7 +195,7 @@ async fn run(miniserve_config: MiniserveConfig) -> Result<(), ContextualError> { let log_level = if miniserve_config.verbose { simplelog::LevelFilter::Info } else { - simplelog::LevelFilter::Error + simplelog::LevelFilter::Warn }; if simplelog::TermLogger::init( -- cgit v1.2.3 From 3c5a2de4308975af2ba3a831b638705c424fd2f8 Mon Sep 17 00:00:00 2001 From: Sven-Hendrik Haase Date: Sun, 28 Mar 2021 21:46:06 +0200 Subject: Change start message without arguments to be a bit more clear --- src/main.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/main.rs b/src/main.rs index 1b896da..5fadb07 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,7 +12,7 @@ use actix_web::{ use actix_web::{middleware, App, HttpRequest, HttpResponse}; use actix_web_httpauth::middleware::HttpAuthentication; use http::header::HeaderMap; -use log::error; +use log::{error, warn}; use structopt::clap::crate_version; use structopt::StructOpt; use yansi::{Color, Paint}; @@ -266,9 +266,9 @@ async fn run(miniserve_config: MiniserveConfig) -> 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." + 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() -- cgit v1.2.3 From 906af1587144dd4b3caecacdff5ea834012cffa4 Mon Sep 17 00:00:00 2001 From: Sven-Hendrik Haase Date: Sun, 28 Mar 2021 22:41:07 +0200 Subject: Refuse to start without explicit path if not attached to interactive terminal --- src/archive.rs | 27 ++++++++++++++++++--------- src/errors.rs | 52 +++++++++++++++++++++++++++++----------------------- src/file_upload.rs | 4 +--- src/main.rs | 16 +++++++++++++--- 4 files changed, 61 insertions(+), 38 deletions(-) (limited to 'src') diff --git a/src/archive.rs b/src/archive.rs index 894ee3f..4df3a31 100644 --- a/src/archive.rs +++ b/src/archive.rs @@ -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/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), - /// 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), - /// 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 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>>> { 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/main.rs b/src/main.rs index 5fadb07..7cd4d3e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -220,8 +220,8 @@ async fn run(miniserve_config: MiniserveConfig) -> 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(), )); } } @@ -266,6 +266,14 @@ async fn run(miniserve_config: MiniserveConfig) -> Result<(), ContextualError> { version = crate_version!() ); if !miniserve_config.path_explicitly_chosen { + // 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." @@ -361,7 +369,9 @@ async fn run(miniserve_config: MiniserveConfig) -> 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)) -- cgit v1.2.3 From 9b278a471a7c9f0587ee421335a2f675afe90eb7 Mon Sep 17 00:00:00 2001 From: Sven-Hendrik Haase Date: Sun, 28 Mar 2021 22:46:32 +0200 Subject: Bump deps --- src/main.rs | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/main.rs b/src/main.rs index 7cd4d3e..84a4cb8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -202,6 +202,7 @@ async fn run(miniserve_config: MiniserveConfig) -> Result<(), ContextualError> { log_level, simplelog::Config::default(), simplelog::TerminalMode::Mixed, + simplelog::ColorChoice::Auto, ) .is_err() { -- cgit v1.2.3 From dd6ca41fcaa1f67875dbe3e9c6a047813075e391 Mon Sep 17 00:00:00 2001 From: Ali MJ Al-Nasrawy Date: Tue, 6 Apr 2021 20:15:04 +0300 Subject: Fix percent encoding for URL Use proper percent-encoding charset for each context. --- src/listing.rs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/listing.rs b/src/listing.rs index 66aea6b..2a62fd6 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, 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)] @@ -213,7 +222,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(), }; @@ -256,7 +265,7 @@ pub fn directory_listing( 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_url = utf8_percent_encode(&p.to_string_lossy(), PATH).to_string(); let file_name = entry.file_name().to_string_lossy().to_string(); // if file is a directory, add '/' to the end of the name -- cgit v1.2.3 From 1beb4c992393b774b11cc3bad444c104dd263562 Mon Sep 17 00:00:00 2001 From: Ali MJ Al-Nasrawy Date: Thu, 8 Apr 2021 09:45:37 +0300 Subject: Avoid double-encoding file URL Now that the '%' char itself is accepted in the file name and is encoded into '%25', this exposed a previously silent bug: `base` is already percent-encoded but it is encoded again when setting `file_url`. This produces erroneous URLs such as: '/%2523/x.y' instead of '/%23/x.y' for the path '/#/x.y' --- src/listing.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/listing.rs b/src/listing.rs index 2a62fd6..8c01b4b 100644 --- a/src/listing.rs +++ b/src/listing.rs @@ -15,7 +15,7 @@ use strum_macros::{Display, EnumString}; use crate::archive::CompressionMethod; use crate::errors::{self, ContextualError}; use crate::renderer; -use percent_encode_sets::{PATH, PATH_SEGMENT}; +use percent_encode_sets::PATH_SEGMENT; /// "percent-encode sets" as defined by WHATWG specs: /// https://url.spec.whatwg.org/#percent-encoded-bytes @@ -260,13 +260,12 @@ 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(), PATH).to_string(); let file_name = entry.file_name().to_string_lossy().to_string(); + 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) = entry.metadata() { -- cgit v1.2.3 From 8b35b83a930405251f00579860789715fdfb85d1 Mon Sep 17 00:00:00 2001 From: Ali MJ Al-Nasrawy Date: Fri, 9 Apr 2021 14:34:48 +0300 Subject: Fix breadcrumbs for RTL languages --- src/renderer.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/renderer.rs b/src/renderer.rs index c9ec9cd..b193c6f 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -80,14 +80,14 @@ 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) + bdi { (el.name) } } } "/" -- cgit v1.2.3 From 32a88fe41457acc8f1525a951dbe40d39ffe8ce3 Mon Sep 17 00:00:00 2001 From: Ali MJ Al-Nasrawy Date: Fri, 9 Apr 2021 14:52:03 +0300 Subject: Fix breadcrumbs mess on mobile On mobile devices, 'display: block' is set for '.directory' class. While this neccessary to make the whole row for directories entry clickable, it distorts the links in breadcrumbs. --- src/renderer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/renderer.rs b/src/renderer.rs index b193c6f..c99ea67 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -86,7 +86,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.directory href=(parametrized_link(&el.link, sort_method, sort_order)) { + a href=(parametrized_link(&el.link, sort_method, sort_order)) { bdi { (el.name) } } } -- cgit v1.2.3 From 2bae301ed8efcf4239849a45b94cdc42e398b905 Mon Sep 17 00:00:00 2001 From: Dean Li Date: Sun, 11 Apr 2021 11:00:16 +0800 Subject: Separate tar archive and tar flags It used to have one flag (-r) to enable both tar archive and tar. Now it has two flags [ -r: for tar, -g: for tar archive]. Related to #451 --- src/archive.rs | 9 +++++++-- src/args.rs | 6 +++++- src/listing.rs | 4 +++- src/main.rs | 8 +++++++- src/renderer.rs | 5 +++-- 5 files changed, 25 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/archive.rs b/src/archive.rs index 4df3a31..28d26b5 100644 --- a/src/archive.rs +++ b/src/archive.rs @@ -53,10 +53,15 @@ impl CompressionMethod { } } - pub fn is_enabled(self, tar_enabled: bool, zip_enabled: bool) -> bool { + pub fn is_enabled( + self, + tar_enabled: bool, + tar_archive_enabled: bool, + zip_enabled: bool, + ) -> bool { match self { - CompressionMethod::TarGz => tar_enabled, CompressionMethod::Tar => tar_enabled, + CompressionMethod::TarGz => tar_archive_enabled, CompressionMethod::Zip => zip_enabled, } } diff --git a/src/args.rs b/src/args.rs index 81e49dc..bf30254 100644 --- a/src/args.rs +++ b/src/args.rs @@ -99,10 +99,14 @@ pub struct CliArgs { #[structopt(short = "o", long = "overwrite-files")] pub overwrite_files: bool, - /// Enable tar archive generation + /// Enable tar generation #[structopt(short = "r", long = "enable-tar")] pub enable_tar: bool, + /// Enable tar archive generation + #[structopt(short = "g", long = "enable-tar-archive")] + pub enable_tar_archive: bool, + /// Enable zip archive generation /// /// WARNING: Zipping large directories can result in out-of-memory exception diff --git a/src/listing.rs b/src/listing.rs index 66aea6b..868817c 100644 --- a/src/listing.rs +++ b/src/listing.rs @@ -158,6 +158,7 @@ pub fn directory_listing( show_qrcode: bool, upload_route: String, tar_enabled: bool, + tar_archive_enabled: bool, zip_enabled: bool, dirs_first: bool, hide_version_footer: bool, @@ -330,7 +331,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_archive_enabled, zip_enabled) { return Ok(ServiceResponse::new( req.clone(), HttpResponse::Forbidden() @@ -413,6 +414,7 @@ pub fn directory_listing( &encoded_dir, breadcrumbs, tar_enabled, + tar_archive_enabled, zip_enabled, hide_version_footer, ) diff --git a/src/main.rs b/src/main.rs index 84a4cb8..a00ac3c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -90,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 tar is disabled pub tar_enabled: bool, + /// If false, creation of tar archives is disabled + pub tar_archive_enabled: bool, + /// If false, creation of zip archives is disabled pub zip_enabled: bool, @@ -161,6 +164,7 @@ impl MiniserveConfig { show_qrcode: args.qrcode, file_upload: args.file_upload, tar_enabled: args.enable_tar, + tar_archive_enabled: args.enable_tar_archive, zip_enabled: args.enable_zip, dirs_first: args.dirs_first, title: args.title, @@ -411,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_archive_enabled = conf.tar_archive_enabled; let zip_enabled = conf.zip_enabled; let dirs_first = conf.dirs_first; let hide_version_footer = conf.hide_version_footer; @@ -453,6 +458,7 @@ fn configure_app(app: &mut web::ServiceConfig, conf: &MiniserveConfig) { show_qrcode, u_r.clone(), tar_enabled, + tar_archive_enabled, zip_enabled, dirs_first, hide_version_footer, diff --git a/src/renderer.rs b/src/renderer.rs index c9ec9cd..71a929a 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -26,6 +26,7 @@ pub fn page( encoded_dir: &str, breadcrumbs: Vec, tar_enabled: bool, + tar_archive_enabled: bool, zip_enabled: bool, hide_version_footer: bool, ) -> Markup { @@ -94,10 +95,10 @@ pub fn page( } } div.toolbar { - @if tar_enabled || zip_enabled { + @if tar_enabled || tar_archive_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_archive_enabled, zip_enabled) { (archive_button(compression_method, sort_method, sort_order)) } } -- cgit v1.2.3 From c9506c72ff368867982d138a686648fa302b116d Mon Sep 17 00:00:00 2001 From: Dean Li Date: Sun, 18 Apr 2021 11:22:52 +0800 Subject: Change naming of uncompressed/compressed tarballs Use following terminology: uncompressed tarballs => `uncompressed tar archives` compressed ones => `gz-compressed tar archives` --- src/archive.rs | 9 ++------- src/args.rs | 8 ++++---- src/listing.rs | 6 +++--- src/main.rs | 12 ++++++------ src/renderer.rs | 6 +++--- 5 files changed, 18 insertions(+), 23 deletions(-) (limited to 'src') diff --git a/src/archive.rs b/src/archive.rs index 28d26b5..e53aea8 100644 --- a/src/archive.rs +++ b/src/archive.rs @@ -53,15 +53,10 @@ impl CompressionMethod { } } - pub fn is_enabled( - self, - tar_enabled: bool, - tar_archive_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_gz_enabled, CompressionMethod::Tar => tar_enabled, - CompressionMethod::TarGz => tar_archive_enabled, CompressionMethod::Zip => zip_enabled, } } diff --git a/src/args.rs b/src/args.rs index bf30254..819618f 100644 --- a/src/args.rs +++ b/src/args.rs @@ -99,13 +99,13 @@ pub struct CliArgs { #[structopt(short = "o", long = "overwrite-files")] pub overwrite_files: bool, - /// Enable tar generation + /// Enable uncompressed tar archive generation #[structopt(short = "r", long = "enable-tar")] pub enable_tar: bool, - /// Enable tar archive generation - #[structopt(short = "g", long = "enable-tar-archive")] - pub enable_tar_archive: bool, + /// Enable gz-compressed tar archive generation + #[structopt(short = "g", long = "enable-tar-gz")] + pub enable_tar_gz: bool, /// Enable zip archive generation /// diff --git a/src/listing.rs b/src/listing.rs index 868817c..cff4f9a 100644 --- a/src/listing.rs +++ b/src/listing.rs @@ -158,7 +158,7 @@ pub fn directory_listing( show_qrcode: bool, upload_route: String, tar_enabled: bool, - tar_archive_enabled: bool, + tar_gz_enabled: bool, zip_enabled: bool, dirs_first: bool, hide_version_footer: bool, @@ -331,7 +331,7 @@ pub fn directory_listing( } if let Some(compression_method) = query_params.download { - if !compression_method.is_enabled(tar_enabled, tar_archive_enabled, zip_enabled) { + if !compression_method.is_enabled(tar_enabled, tar_gz_enabled, zip_enabled) { return Ok(ServiceResponse::new( req.clone(), HttpResponse::Forbidden() @@ -414,7 +414,7 @@ pub fn directory_listing( &encoded_dir, breadcrumbs, tar_enabled, - tar_archive_enabled, + tar_gz_enabled, zip_enabled, hide_version_footer, ) diff --git a/src/main.rs b/src/main.rs index a00ac3c..f174d57 100644 --- a/src/main.rs +++ b/src/main.rs @@ -90,11 +90,11 @@ pub struct MiniserveConfig { /// Enable upload to override existing files pub overwrite_files: bool, - /// If false, creation of tar is disabled + /// If false, creation of uncompressed tar archives is disabled pub tar_enabled: bool, - /// If false, creation of tar archives is disabled - pub tar_archive_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, @@ -164,7 +164,7 @@ impl MiniserveConfig { show_qrcode: args.qrcode, file_upload: args.file_upload, tar_enabled: args.enable_tar, - tar_archive_enabled: args.enable_tar_archive, + tar_gz_enabled: args.enable_tar_gz, zip_enabled: args.enable_zip, dirs_first: args.dirs_first, title: args.title, @@ -415,7 +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_archive_enabled = conf.tar_archive_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; @@ -458,7 +458,7 @@ fn configure_app(app: &mut web::ServiceConfig, conf: &MiniserveConfig) { show_qrcode, u_r.clone(), tar_enabled, - tar_archive_enabled, + tar_gz_enabled, zip_enabled, dirs_first, hide_version_footer, diff --git a/src/renderer.rs b/src/renderer.rs index 71a929a..5cb5452 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -26,7 +26,7 @@ pub fn page( encoded_dir: &str, breadcrumbs: Vec, tar_enabled: bool, - tar_archive_enabled: bool, + tar_gz_enabled: bool, zip_enabled: bool, hide_version_footer: bool, ) -> Markup { @@ -95,10 +95,10 @@ pub fn page( } } div.toolbar { - @if tar_enabled || tar_archive_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, tar_archive_enabled, zip_enabled) { + @if compression_method.is_enabled(tar_enabled, tar_gz_enabled, zip_enabled) { (archive_button(compression_method, sort_method, sort_order)) } } -- cgit v1.2.3