aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/args.rs2
-rw-r--r--src/auth.rs7
-rw-r--r--src/config.rs10
-rw-r--r--src/errors.rs3
-rw-r--r--src/file_op.rs34
-rw-r--r--src/listing.rs16
-rw-r--r--src/main.rs45
-rw-r--r--src/renderer.rs81
8 files changed, 173 insertions, 25 deletions
diff --git a/src/args.rs b/src/args.rs
index 6874e25..d078ef2 100644
--- a/src/args.rs
+++ b/src/args.rs
@@ -367,7 +367,7 @@ pub struct CliArgs {
#[arg(long, env = "MINISERVE_ENABLE_WEBDAV", conflicts_with = "no_symlinks")]
pub enable_webdav: bool,
- /// Show served file size in exact bytes.
+ /// Show served file size in exact bytes
#[arg(long, default_value_t = SizeDisplay::Human, env = "MINISERVE_SIZE_DISPLAY")]
pub size_display: SizeDisplay,
}
diff --git a/src/auth.rs b/src/auth.rs
index fa28c4a..3bd9313 100644
--- a/src/auth.rs
+++ b/src/auth.rs
@@ -1,4 +1,4 @@
-use actix_web::{HttpMessage, dev::ServiceRequest};
+use actix_web::{HttpMessage, dev::ServiceRequest, web};
use actix_web_httpauth::extractors::basic::BasicAuth;
use sha2::{Digest, Sha256, Sha512};
@@ -77,7 +77,10 @@ pub async fn handle_auth(
req: ServiceRequest,
cred: BasicAuth,
) -> actix_web::Result<ServiceRequest, (actix_web::Error, ServiceRequest)> {
- let required_auth = &req.app_data::<crate::MiniserveConfig>().unwrap().auth;
+ let required_auth = &req
+ .app_data::<web::Data<crate::MiniserveConfig>>()
+ .unwrap()
+ .auth;
req.extensions_mut().insert(CurrentUser {
name: cred.user_id().to_string(),
diff --git a/src/config.rs b/src/config.rs
index efec515..eecc45e 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -24,7 +24,7 @@ const ROUTE_ALPHABET: [char; 16] = [
'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'a', 'b', 'c', 'd', 'e', 'f',
];
-#[derive(Clone)]
+#[derive(Debug, Clone)]
/// Configuration of the Miniserve application
pub struct MiniserveConfig {
/// Enable verbose mode
@@ -66,6 +66,9 @@ pub struct MiniserveConfig {
/// Well-known healthcheck route (prefixed if route_prefix is provided)
pub healthcheck_route: String,
+ /// Well-known API route (prefixed if route_prefix is provided)
+ pub api_route: String,
+
/// Well-known favicon route (prefixed if route_prefix is provided)
pub favicon_route: String,
@@ -206,15 +209,17 @@ impl MiniserveConfig {
// If --random-route is enabled, in order to not leak the random generated route, we must not use it
// as static files prefix.
// Otherwise, we should apply route_prefix to static files.
- let (healthcheck_route, favicon_route, css_route) = if args.random_route {
+ let (healthcheck_route, api_route, favicon_route, css_route) = if args.random_route {
(
"/__miniserve_internal/healthcheck".into(),
+ "/__miniserve_internal/api".into(),
"/__miniserve_internal/favicon.svg".into(),
"/__miniserve_internal/style.css".into(),
)
} else {
(
format!("{}/{}", route_prefix, "__miniserve_internal/healthcheck"),
+ format!("{}/{}", route_prefix, "__miniserve_internal/api"),
format!("{}/{}", route_prefix, "__miniserve_internal/favicon.svg"),
format!("{}/{}", route_prefix, "__miniserve_internal/style.css"),
)
@@ -305,6 +310,7 @@ impl MiniserveConfig {
default_sorting_order: args.default_sorting_order,
route_prefix,
healthcheck_route,
+ api_route,
favicon_route,
css_route,
default_color_scheme,
diff --git a/src/errors.rs b/src/errors.rs
index e35e8a8..3ac7da2 100644
--- a/src/errors.rs
+++ b/src/errors.rs
@@ -6,6 +6,7 @@ use actix_web::{
dev::{ResponseHead, ServiceRequest, ServiceResponse},
http::{StatusCode, header},
middleware::Next,
+ web,
};
use thiserror::Error;
@@ -159,7 +160,7 @@ fn map_error_page(req: &HttpRequest, head: &mut ResponseHead, body: BoxBody) ->
_ => return BoxBody::new(error_msg),
};
- let conf = req.app_data::<MiniserveConfig>().unwrap();
+ let conf = req.app_data::<web::Data<MiniserveConfig>>().unwrap();
let return_address = req
.headers()
.get(header::REFERER)
diff --git a/src/file_op.rs b/src/file_op.rs
index 4319410..149cd2a 100644
--- a/src/file_op.rs
+++ b/src/file_op.rs
@@ -4,6 +4,7 @@ use std::io::ErrorKind;
use std::path::{Component, Path, PathBuf};
use actix_web::{HttpRequest, HttpResponse, http::header, web};
+use async_walkdir::{Filtering, WalkDir};
use futures::{StreamExt, TryStreamExt};
use log::{info, warn};
use serde::Deserialize;
@@ -38,6 +39,37 @@ impl FileHash {
}
}
+/// Get the recursively calculated dir size for a given dir
+///
+/// Expects `dir` to be sanitized. This function doesn't do any sanitization itself.
+pub async fn recursive_dir_size(dir: &Path) -> Result<u64, RuntimeError> {
+ let mut entries = WalkDir::new(dir).filter(|entry| async move {
+ if let Ok(metadata) = entry.metadata().await {
+ if metadata.is_file() {
+ return Filtering::Continue;
+ }
+ }
+ Filtering::Ignore
+ });
+
+ let mut total_size = 0;
+ loop {
+ match entries.next().await {
+ Some(Ok(entry)) => {
+ if let Ok(metadata) = entry.metadata().await {
+ total_size += metadata.len();
+ }
+ }
+ Some(Err(e)) => {
+ warn!("Error trying to read file when calculating dir size: {e}");
+ return Err(RuntimeError::InvalidPathError(e.to_string()));
+ }
+ None => break,
+ }
+ }
+ Ok(total_size)
+}
+
/// Saves file data from a multipart form field (`field`) to `file_path`. Optionally overwriting
/// existing file and comparing the uploaded file checksum to the user provided `file_hash`.
///
@@ -329,7 +361,7 @@ pub async fn upload_file(
query: web::Query<FileOpQueryParameters>,
payload: web::Payload,
) -> Result<HttpResponse, RuntimeError> {
- let conf = req.app_data::<MiniserveConfig>().unwrap();
+ let conf = req.app_data::<web::Data<MiniserveConfig>>().unwrap();
let upload_path = sanitize_path(&query.path, conf.show_hidden).ok_or_else(|| {
RuntimeError::InvalidPathError("Invalid value for 'path' parameter".to_string())
})?;
diff --git a/src/listing.rs b/src/listing.rs
index 025ae86..6e50ba1 100644
--- a/src/listing.rs
+++ b/src/listing.rs
@@ -4,7 +4,7 @@ use std::path::{Component, Path};
use std::time::SystemTime;
use actix_web::{
- HttpMessage, HttpRequest, HttpResponse, dev::ServiceResponse, http::Uri, web::Query,
+ HttpMessage, HttpRequest, HttpResponse, dev::ServiceResponse, http::Uri, web, web::Query,
};
use bytesize::ByteSize;
use clap::ValueEnum;
@@ -51,7 +51,7 @@ pub struct ListingQueryParameters {
}
/// Available sorting methods
-#[derive(Deserialize, Default, Clone, EnumString, Display, Copy, ValueEnum)]
+#[derive(Debug, Deserialize, Default, Clone, EnumString, Display, Copy, ValueEnum)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum SortingMethod {
@@ -67,7 +67,7 @@ pub enum SortingMethod {
}
/// Available sorting orders
-#[derive(Deserialize, Default, Clone, EnumString, Display, Copy, ValueEnum)]
+#[derive(Debug, Deserialize, Default, Clone, EnumString, Display, Copy, ValueEnum)]
pub enum SortingOrder {
/// Ascending order
#[serde(alias = "asc")]
@@ -81,8 +81,9 @@ pub enum SortingOrder {
Desc,
}
-#[derive(PartialEq, Eq)]
/// Possible entry types
+#[derive(PartialEq, Clone, Display, Eq)]
+#[strum(serialize_all = "snake_case")]
pub enum EntryType {
/// Entry is a directory
Directory,
@@ -158,7 +159,10 @@ impl Breadcrumb {
}
pub async fn file_handler(req: HttpRequest) -> actix_web::Result<actix_files::NamedFile> {
- let path = &req.app_data::<crate::MiniserveConfig>().unwrap().path;
+ let path = &req
+ .app_data::<web::Data<crate::MiniserveConfig>>()
+ .unwrap()
+ .path;
actix_files::NamedFile::open(path).map_err(Into::into)
}
@@ -171,7 +175,7 @@ pub fn directory_listing(
let extensions = req.extensions();
let current_user: Option<&CurrentUser> = extensions.get::<CurrentUser>();
- let conf = req.app_data::<crate::MiniserveConfig>().unwrap();
+ let conf = req.app_data::<web::Data<crate::MiniserveConfig>>().unwrap();
if conf.disable_indexing {
return Ok(ServiceResponse::new(
req.clone(),
diff --git a/src/main.rs b/src/main.rs
index 856d22d..adda3f7 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -14,6 +14,7 @@ use actix_web::{
};
use actix_web_httpauth::middleware::HttpAuthentication;
use anyhow::Result;
+use bytesize::ByteSize;
use clap::{CommandFactory, Parser, crate_version};
use colored::*;
use dav_server::{
@@ -21,7 +22,8 @@ use dav_server::{
actix::{DavRequest, DavResponse},
};
use fast_qr::QRBuilder;
-use log::{error, warn};
+use log::{error, info, warn};
+use serde::Deserialize;
mod archive;
mod args;
@@ -38,6 +40,7 @@ mod webdav_fs;
use crate::config::MiniserveConfig;
use crate::errors::{RuntimeError, StartupError};
+use crate::file_op::recursive_dir_size;
use crate::webdav_fs::RestrictedFs;
static STYLESHEET: &str = grass::include!("data/style.scss");
@@ -215,7 +218,7 @@ async fn run(miniserve_config: MiniserveConfig) -> Result<(), StartupError> {
let srv = actix_web::HttpServer::new(move || {
App::new()
.wrap(configure_header(&inside_config.clone()))
- .app_data(inside_config.clone())
+ .app_data(web::Data::new(inside_config.clone()))
.app_data(stylesheet.clone())
.wrap(from_fn(errors::error_page_middleware))
.wrap(middleware::Logger::default())
@@ -224,6 +227,7 @@ async fn run(miniserve_config: MiniserveConfig) -> Result<(), StartupError> {
middleware::Compress::default(),
))
.route(&inside_config.healthcheck_route, web::get().to(healthcheck))
+ .route(&inside_config.api_route, web::post().to(api))
.route(&inside_config.favicon_route, web::get().to(favicon))
.route(&inside_config.css_route, web::get().to(css))
.service(
@@ -353,7 +357,7 @@ fn configure_app(app: &mut web::ServiceConfig, conf: &MiniserveConfig) {
files = files.default_handler(fn_service(|req: ServiceRequest| async {
let (req, _) = req.into_parts();
let conf = req
- .app_data::<MiniserveConfig>()
+ .app_data::<web::Data<MiniserveConfig>>()
.expect("Could not get miniserve config");
let mut path_base = req.path()[1..].to_string();
if path_base.ends_with('/') {
@@ -438,6 +442,41 @@ async fn healthcheck() -> impl Responder {
HttpResponse::Ok().body("OK")
}
+#[derive(Deserialize, Debug)]
+enum ApiCommand {
+ /// Request the size of a particular directory
+ DirSize(String),
+}
+
+/// This "API" is pretty shitty but frankly miniserve doesn't really need a very fancy API. Or at
+/// least I hope so.
+async fn api(
+ command: web::Json<ApiCommand>,
+ config: web::Data<MiniserveConfig>,
+) -> Result<impl Responder, RuntimeError> {
+ match command.into_inner() {
+ ApiCommand::DirSize(dir) => {
+ // Convert the relative dir to an absolute path on the system
+ let sanitized_path =
+ file_utils::sanitize_path(&dir, true).expect("Expected a path to directory");
+ let full_path = config
+ .path
+ .canonicalize()
+ .expect("Couldn't canonicalize path")
+ .join(sanitized_path);
+ info!("Requested directory listing for {full_path:?}");
+
+ let dir_size = recursive_dir_size(&full_path).await?;
+ if config.show_exact_bytes {
+ Ok(format!("{dir_size} B"))
+ } else {
+ let dir_size = ByteSize::b(dir_size);
+ Ok(dir_size.to_string())
+ }
+ }
+ }
+}
+
async fn favicon() -> impl Responder {
let logo = include_str!("../data/logo.svg");
HttpResponse::Ok()
diff --git a/src/renderer.rs b/src/renderer.rs
index a59f458..d6b01c8 100644
--- a/src/renderer.rs
+++ b/src/renderer.rs
@@ -52,7 +52,7 @@ pub fn page(
html! {
(DOCTYPE)
html {
- (page_header(&title_path, conf.file_upload, conf.web_upload_concurrency, &conf.favicon_route, &conf.css_route))
+ (page_header(&title_path, conf.file_upload, conf.web_upload_concurrency, &conf.api_route, &conf.favicon_route, &conf.css_route))
body #drop-container
{
@@ -525,11 +525,12 @@ fn entry_row(
show_exact_bytes: bool,
) -> Markup {
html! {
- tr {
+ @let entry_type = entry.entry_type.clone();
+ tr .{ "entry-type-" (entry_type) } {
td {
p {
@if entry.is_dir() {
- @if let Some(symlink_dest) = entry.symlink_info {
+ @if let Some(ref symlink_dest) = entry.symlink_info {
a.symlink href=(parametrized_link(&entry.link, sort_method, sort_order, raw)) {
(entry.name) "/"
span.symlink-symbol { }
@@ -541,7 +542,7 @@ fn entry_row(
}
}
} @else if entry.is_file() {
- @if let Some(symlink_dest) = entry.symlink_info {
+ @if let Some(ref symlink_dest) = entry.symlink_info {
a.symlink href=(&entry.link) {
(entry.name)
span.symlink-symbol { }
@@ -624,6 +625,7 @@ fn page_header(
title: &str,
file_upload: bool,
web_file_concurrency: usize,
+ api_route: &str,
favicon_route: &str,
css_route: &str,
) -> Markup {
@@ -639,8 +641,8 @@ fn page_header(
title { (title) }
- (PreEscaped(r#"
- <script>
+ script {
+ (PreEscaped(r#"
// updates the color scheme by setting the theme data attribute
// on body and saving the new theme to local storage
function updateColorScheme(name) {
@@ -663,8 +665,69 @@ fn page_header(
addEventListener("load", loadColorScheme);
// load saved theme when local storage is changed (synchronize between tabs)
addEventListener("storage", loadColorScheme);
- </script>
- "#))
+ "#))
+ }
+
+ script {
+ (format!("const API_ROUTE = '{api_route}';"))
+ (PreEscaped(r#"
+ let dirSizeCache = {};
+
+ // Query the directory size from the miniserve API
+ function fetchDirSize(dir) {
+ return fetch(API_ROUTE, {
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json'
+ },
+ method: 'POST',
+ body: JSON.stringify({
+ DirSize: dir
+ })
+ }).then(resp => resp.text())
+ }
+
+ // Initialize shimmer effects for .size-cell elements in .entry-type-directory rows
+ //
+ // TODO: Perhaps it'd be better to statically do this during html generation in
+ // entry_row()?
+ function initializeLoadingIndicators() {
+ const directoryCells = document.querySelectorAll('tr.entry-type-directory .size-cell');
+
+ directoryCells.forEach(cell => {
+ // Add a loading indicator to each cell
+ const loadingIndicator = document.createElement('div');
+ loadingIndicator.className = 'loading-indicator';
+ cell.appendChild(loadingIndicator);
+ });
+ }
+
+ function updateSizeCells() {
+ const directoryCells = document.querySelectorAll('tr.entry-type-directory .size-cell');
+
+ directoryCells.forEach(cell => {
+ // Get the dir from the sibling anchor tag.
+ const href = cell.parentNode.querySelector('a').href;
+ const target = new URL(href).pathname;
+
+ // First check our local cache
+ if (target in dirSizeCache) {
+ cell.innerHTML = '';
+ cell.textContent = dirSizeCache[target];
+ } else {
+ fetchDirSize(target).then(dir_size => {
+ cell.innerHTML = '';
+ cell.textContent = dir_size;
+ dirSizeCache[target] = dir_size;
+ })
+ .catch(error => console.error("Error fetching dir size:", error));
+ }
+ })
+ }
+ setInterval(updateSizeCells, 1000);
+ window.addEventListener('load', initializeLoadingIndicators);
+ "#))
+ }
@if file_upload {
script {
@@ -1003,7 +1066,7 @@ pub fn render_error(
html! {
(DOCTYPE)
html {
- (page_header(&error_code.to_string(), false, conf.web_upload_concurrency, &conf.favicon_route, &conf.css_route))
+ (page_header(&error_code.to_string(), false, conf.web_upload_concurrency, &conf.api_route, &conf.favicon_route, &conf.css_route))
body
{