aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--Cargo.lock171
-rw-r--r--Cargo.toml1
-rw-r--r--src/handlers.rs183
-rw-r--r--src/main.rs99
-rw-r--r--src/renderer.rs48
5 files changed, 398 insertions, 104 deletions
diff --git a/Cargo.lock b/Cargo.lock
index e744911..9690274 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -31,7 +31,7 @@ dependencies = [
"actix-web",
"bitflags",
"bytes",
- "derive_more",
+ "derive_more 0.99.19",
"futures-core",
"http-range",
"log",
@@ -59,7 +59,7 @@ dependencies = [
"brotli",
"bytes",
"bytestring",
- "derive_more",
+ "derive_more 0.99.19",
"encoding_rs",
"flate2",
"futures-core",
@@ -101,7 +101,7 @@ dependencies = [
"actix-multipart-derive",
"actix-utils",
"actix-web",
- "derive_more",
+ "derive_more 0.99.19",
"futures-core",
"futures-util",
"httparse",
@@ -233,7 +233,7 @@ dependencies = [
"bytestring",
"cfg-if",
"cookie",
- "derive_more",
+ "derive_more 0.99.19",
"encoding_rs",
"futures-core",
"futures-util",
@@ -283,6 +283,53 @@ dependencies = [
]
[[package]]
+name = "actix-web-lab"
+version = "0.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53df243e3d9bae9e2e1078e8639a0e6f4223a4d0cd7ee3b43ab9d25ec0751f88"
+dependencies = [
+ "actix-http",
+ "actix-router",
+ "actix-service",
+ "actix-utils",
+ "actix-web",
+ "actix-web-lab-derive",
+ "ahash",
+ "arc-swap",
+ "bytes",
+ "bytestring",
+ "csv",
+ "derive_more 2.0.1",
+ "form_urlencoded",
+ "futures-core",
+ "futures-util",
+ "http 0.2.12",
+ "impl-more",
+ "itertools",
+ "local-channel",
+ "mime",
+ "pin-project-lite",
+ "regex",
+ "serde",
+ "serde_html_form",
+ "serde_json",
+ "serde_path_to_error",
+ "tokio",
+ "tokio-stream",
+ "tracing",
+]
+
+[[package]]
+name = "actix-web-lab-derive"
+version = "0.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd80fa0bd6217e482112d9d87a05af8e0f8dec9e3aa51f34816f761c5cf7da7"
+dependencies = [
+ "quote",
+ "syn 2.0.98",
+]
+
+[[package]]
name = "addr2line"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -433,6 +480,12 @@ dependencies = [
]
[[package]]
+name = "arc-swap"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
+
+[[package]]
name = "assert_cmd"
version = "2.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -928,6 +981,27 @@ dependencies = [
]
[[package]]
+name = "csv"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf"
+dependencies = [
+ "csv-core",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "csv-core"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
name = "darling"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1035,6 +1109,27 @@ dependencies = [
]
[[package]]
+name = "derive_more"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
+dependencies = [
+ "derive_more-impl",
+]
+
+[[package]]
+name = "derive_more-impl"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.98",
+ "unicode-xid",
+]
+
+[[package]]
name = "deunicode"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1093,6 +1188,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
+[[package]]
name = "encoding_rs"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1928,6 +2029,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
+name = "itertools"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+dependencies = [
+ "either",
+]
+
+[[package]]
name = "itoa"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2162,6 +2272,7 @@ dependencies = [
"actix-multipart",
"actix-web",
"actix-web-httpauth",
+ "actix-web-lab",
"alphanumeric-sort",
"anyhow",
"assert_cmd",
@@ -3082,6 +3193,19 @@ dependencies = [
]
[[package]]
+name = "serde_html_form"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d2de91cf02bbc07cde38891769ccd5d4f073d22a40683aa4bc7a95781aaa2c4"
+dependencies = [
+ "form_urlencoded",
+ "indexmap",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
name = "serde_json"
version = "1.0.138"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3094,6 +3218,16 @@ dependencies = [
]
[[package]]
+name = "serde_path_to_error"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a"
+dependencies = [
+ "itoa",
+ "serde",
+]
+
+[[package]]
name = "serde_plain"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3552,6 +3686,17 @@ dependencies = [
]
[[package]]
+name = "tokio-stream"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
name = "tokio-util"
version = "0.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3616,10 +3761,22 @@ checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [
"log",
"pin-project-lite",
+ "tracing-attributes",
"tracing-core",
]
[[package]]
+name = "tracing-attributes"
+version = "0.1.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.98",
+]
+
+[[package]]
name = "tracing-core"
version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3668,6 +3825,12 @@ dependencies = [
]
[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[package]]
name = "unicode_categories"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 1115fef..dc54c7e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -22,6 +22,7 @@ actix-files = "0.6.5"
actix-multipart = "0.7"
actix-web = { version = "4", features = ["macros", "compress-brotli", "compress-gzip", "compress-zstd"], default-features = false }
actix-web-httpauth = "0.8"
+actix-web-lab = "0.24.0"
alphanumeric-sort = "1"
anyhow = "1"
async-walkdir = "2.1.0"
diff --git a/src/handlers.rs b/src/handlers.rs
new file mode 100644
index 0000000..8b4d6e5
--- /dev/null
+++ b/src/handlers.rs
@@ -0,0 +1,183 @@
+use std::time::Duration;
+
+use actix_web::{HttpRequest, HttpResponse, Responder, http::header::ContentType, web};
+use actix_web_lab::sse;
+use bytesize::ByteSize;
+use dav_server::{
+ DavConfig, DavHandler,
+ actix::{DavRequest, DavResponse},
+};
+use log::{error, info, warn};
+use percent_encoding::percent_decode_str;
+use serde::{Deserialize, Serialize};
+use tokio::sync::Mutex;
+use tokio::task::JoinSet;
+
+use crate::{config::MiniserveConfig, errors::RuntimeError};
+use crate::{file_op::recursive_dir_size, file_utils};
+
+pub async fn dav_handler(req: DavRequest, davhandler: web::Data<DavHandler>) -> DavResponse {
+ if let Some(prefix) = req.prefix() {
+ let config = DavConfig::new().strip_prefix(prefix);
+ davhandler.handle_with(config, req.request).await.into()
+ } else {
+ davhandler.handle(req.request).await.into()
+ }
+}
+
+pub async fn error_404(req: HttpRequest) -> Result<HttpResponse, RuntimeError> {
+ Err(RuntimeError::RouteNotFoundError(req.path().to_string()))
+}
+
+pub async fn healthcheck() -> impl Responder {
+ HttpResponse::Ok().body("OK")
+}
+
+#[derive(Deserialize, Debug)]
+pub enum ApiCommand {
+ /// Request the size of a particular directory
+ CalculateDirSizes(Vec<String>),
+}
+
+pub type DirSizeJoinSet = JoinSet<Result<DirSize, RuntimeError>>;
+
+// Holds the result of a calculated dir size
+#[derive(Debug, Clone)]
+pub struct DirSize {
+ /// The web path of the dir (not the filesystem path)
+ pub web_path: String,
+
+ /// The calculcated recursive size of the dir
+ pub size: u64,
+}
+
+// Reply for a calculated dir size
+#[derive(Debug, Clone, Serialize)]
+pub struct DirSizeReply {
+ /// The web path of the dir (not the filesystem path)
+ pub web_path: String,
+
+ /// The formatted size of the dir
+ pub size: String,
+}
+
+// Reply to check whether the client is still connected
+//
+// If the client has disconnected, we can cancel all the tasks and save some compute.
+#[derive(Debug, Clone, Serialize)]
+pub struct HeartbeatReply;
+
+/// SSE API route that yields an event stream that clients can subscribe to
+pub async fn api_sse(
+ config: web::Data<MiniserveConfig>,
+ task_joinset: web::Data<Mutex<DirSizeJoinSet>>,
+) -> impl Responder {
+ let (sender, receiver) = tokio::sync::mpsc::channel(2);
+
+ actix_web::rt::spawn(async move {
+ loop {
+ let msg = match task_joinset.lock().await.try_join_next() {
+ Some(Ok(Ok(finished_task))) => {
+ let dir_size = if config.show_exact_bytes {
+ format!("{} B", finished_task.size)
+ } else {
+ ByteSize::b(finished_task.size).to_string()
+ };
+
+ let dir_size_reply = DirSizeReply {
+ web_path: finished_task.web_path,
+ size: dir_size,
+ };
+
+ sse::Data::new_json(dir_size_reply)
+ .expect("Couldn't serialize as JSON")
+ .event("dir-size")
+ }
+ Some(Ok(Err(e))) => {
+ error!("Some error during dir size calculation: {e}");
+ break;
+ }
+ Some(Err(e)) => {
+ error!("Some error during dir size calculation joining: {e}");
+ break;
+ }
+ None => sse::Data::new_json(HeartbeatReply)
+ .expect("Couldn't serialize as JSON")
+ .event("heartbeat"),
+ };
+
+ if sender.send(msg.into()).await.is_err() {
+ warn!("Client disconnected; could not send SSE message");
+ break;
+ }
+
+ tokio::time::sleep(Duration::from_secs(1)).await;
+ }
+ });
+
+ sse::Sse::from_infallible_receiver(receiver).with_keep_alive(Duration::from_secs(3))
+}
+
+async fn handle_dir_size_tasks(
+ dirs: Vec<String>,
+ config: &MiniserveConfig,
+ task_joinset: web::Data<Mutex<DirSizeJoinSet>>,
+) -> Result<(), RuntimeError> {
+ for dir in dirs {
+ // The dir argument might be percent-encoded so let's decode it just in case.
+ let decoded_path = percent_decode_str(&dir)
+ .decode_utf8()
+ .map_err(|e| RuntimeError::ParseError(dir.clone(), e.to_string()))?;
+
+ // Convert the relative dir to an absolute path on the system.
+ let sanitized_path =
+ file_utils::sanitize_path(&*decoded_path, true).expect("Expected a path to directory");
+
+ let full_path = config
+ .path
+ .canonicalize()
+ .expect("Couldn't canonicalize path")
+ .join(sanitized_path);
+ info!("Requested directory size for {full_path:?}");
+
+ let mut joinset = task_joinset.lock().await;
+ joinset.spawn(async move {
+ recursive_dir_size(&full_path).await.map(|dir_size| {
+ info!("Finished dir size calculation for {full_path:?}");
+ DirSize {
+ web_path: dir,
+ size: dir_size,
+ }
+ })
+ });
+ }
+ Ok(())
+}
+
+/// This "API" is pretty shitty but frankly miniserve doesn't really need a very fancy API. Or at
+/// least I hope so.
+pub async fn api_command(
+ command: web::Json<ApiCommand>,
+ config: web::Data<MiniserveConfig>,
+ task_joinset: web::Data<Mutex<DirSizeJoinSet>>,
+) -> Result<impl Responder, RuntimeError> {
+ match command.into_inner() {
+ ApiCommand::CalculateDirSizes(dirs) => {
+ handle_dir_size_tasks(dirs, &config, task_joinset).await?;
+ Ok("Directories are being calculated")
+ }
+ }
+}
+
+pub async fn favicon() -> impl Responder {
+ let logo = include_str!("../data/logo.svg");
+ HttpResponse::Ok()
+ .insert_header(ContentType(mime::IMAGE_SVG))
+ .body(logo)
+}
+
+pub async fn css(stylesheet: web::Data<String>) -> impl Responder {
+ HttpResponse::Ok()
+ .insert_header(ContentType(mime::TEXT_CSS))
+ .body(stylesheet.to_string())
+}
diff --git a/src/main.rs b/src/main.rs
index cd54bd8..3fe1410 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -6,25 +6,20 @@ use std::time::Duration;
use actix_files::NamedFile;
use actix_web::middleware::from_fn;
use actix_web::{
- App, HttpRequest, HttpResponse, Responder,
+ App,
dev::{ServiceRequest, ServiceResponse, fn_service},
guard,
- http::{Method, header::ContentType},
+ http::Method,
middleware, web,
};
use actix_web_httpauth::middleware::HttpAuthentication;
use anyhow::Result;
-use bytesize::ByteSize;
use clap::{CommandFactory, Parser, crate_version};
use colored::*;
-use dav_server::{
- DavConfig, DavHandler, DavMethodSet,
- actix::{DavRequest, DavResponse},
-};
+use dav_server::{DavHandler, DavMethodSet};
use fast_qr::QRBuilder;
-use log::{error, info, warn};
-use percent_encoding::percent_decode_str;
-use serde::Deserialize;
+use log::{error, warn};
+use tokio::sync::Mutex;
mod archive;
mod args;
@@ -34,14 +29,17 @@ mod consts;
mod errors;
mod file_op;
mod file_utils;
+mod handlers;
mod listing;
mod pipe;
mod renderer;
mod webdav_fs;
use crate::config::MiniserveConfig;
-use crate::errors::{RuntimeError, StartupError};
-use crate::file_op::recursive_dir_size;
+use crate::errors::StartupError;
+use crate::handlers::{
+ DirSizeJoinSet, api_command, api_sse, css, dav_handler, error_404, favicon, healthcheck,
+};
use crate::webdav_fs::RestrictedFs;
static STYLESHEET: &str = grass::include!("data/style.scss");
@@ -216,10 +214,13 @@ async fn run(miniserve_config: MiniserveConfig) -> Result<(), StartupError> {
.join("\n"),
);
+ let dir_size_join_set = web::Data::new(Mutex::new(DirSizeJoinSet::new()));
+
let srv = actix_web::HttpServer::new(move || {
App::new()
.wrap(configure_header(&inside_config.clone()))
.app_data(web::Data::new(inside_config.clone()))
+ .app_data(dir_size_join_set.clone())
.app_data(stylesheet.clone())
.wrap(from_fn(errors::error_page_middleware))
.wrap(middleware::Logger::default())
@@ -228,7 +229,8 @@ 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.api_route, web::post().to(api_command))
+ .route(&inside_config.api_route, web::get().to(api_sse))
.route(&inside_config.favicon_route, web::get().to(favicon))
.route(&inside_config.css_route, web::get().to(css))
.service(
@@ -425,74 +427,3 @@ fn configure_app(app: &mut web::ServiceConfig, conf: &MiniserveConfig) {
);
}
}
-
-async fn dav_handler(req: DavRequest, davhandler: web::Data<DavHandler>) -> DavResponse {
- if let Some(prefix) = req.prefix() {
- let config = DavConfig::new().strip_prefix(prefix);
- davhandler.handle_with(config, req.request).await.into()
- } else {
- davhandler.handle(req.request).await.into()
- }
-}
-
-async fn error_404(req: HttpRequest) -> Result<HttpResponse, RuntimeError> {
- Err(RuntimeError::RouteNotFoundError(req.path().to_string()))
-}
-
-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(path) => {
- // The dir argument might be percent-encoded so let's decode it just in case.
- let decoded_path = percent_decode_str(&path)
- .decode_utf8()
- .map_err(|e| RuntimeError::ParseError(path.clone(), e.to_string()))?;
-
- // Convert the relative dir to an absolute path on the system.
- let sanitized_path = file_utils::sanitize_path(&*decoded_path, 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()
- .insert_header(ContentType(mime::IMAGE_SVG))
- .body(logo)
-}
-
-async fn css(stylesheet: web::Data<String>) -> impl Responder {
- HttpResponse::Ok()
- .insert_header(ContentType(mime::TEXT_CSS))
- .body(stylesheet.to_string())
-}
diff --git a/src/renderer.rs b/src/renderer.rs
index 7846388..bcbbdfe 100644
--- a/src/renderer.rs
+++ b/src/renderer.rs
@@ -674,10 +674,8 @@ fn page_header(
script {
(format!("const API_ROUTE = '{api_route}';"))
(PreEscaped(r#"
- let dirSizeCache = {};
-
// Query the directory size from the miniserve API
- function fetchDirSize(dir) {
+ function requestDirSizeCalculation(dirs) {
return fetch(API_ROUTE, {
headers: {
'Accept': 'application/json',
@@ -685,32 +683,50 @@ fn page_header(
},
method: 'POST',
body: JSON.stringify({
- DirSize: dir
+ CalculateDirSizes: dirs
})
}).then(resp => resp.ok ? resp.text() : "~")
}
- function updateSizeCells() {
+ function updateSizeCells(event_data) {
+ console.log("Received dir-size object", event_data);
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.dataset.size = dirSizeCache[target];
- } else {
- fetchDirSize(target).then(dir_size => {
- cell.dataset.size = dir_size;
- dirSizeCache[target] = dir_size;
- })
- .catch(error => console.error("Error fetching dir size:", error));
+ if (target === event_data.web_path) {
+ cell.dataset.size = event_data.size
}
+ });
+ }
+
+ function requestDirSizes() {
+ // Subscribe to the SSE stream
+ let dir_sizes_events = new EventSource(API_ROUTE)
+
+ dir_sizes_events.addEventListener('dir-size', (event) => {
+ updateSizeCells(JSON.parse(event.data));
})
+
+ const directoryCells = document.querySelectorAll('tr.entry-type-directory .size-cell');
+
+ // The list of dirs we'll request to have calculated.
+ let dirs = [];
+
+ directoryCells.forEach(cell => {
+ // Get the dir from the sibling anchor tag.
+ const href = cell.parentNode.querySelector('a').href;
+ const target = new URL(href).pathname;
+
+ dirs.push(target);
+ });
+
+ requestDirSizeCalculation(dirs);
}
- setInterval(updateSizeCells, 1000);
+
+ window.addEventListener('load', requestDirSizes);
"#))
}