aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/archive.rs31
-rw-r--r--src/args.rs121
-rw-r--r--src/errors.rs52
-rw-r--r--src/file_upload.rs4
-rw-r--r--src/listing.rs30
-rw-r--r--src/main.rs132
-rw-r--r--src/renderer.rs13
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))
}
}