From 5245420073d93664d083e33e16b0e54dc4cad684 Mon Sep 17 00:00:00 2001 From: "Elf M. Sternberg" Date: Mon, 18 May 2026 17:33:07 -0700 Subject: [PATCH] Well, it's serving 'I know it exists, but I can't show it to you.' --- Cargo.lock | 1 + demo/Cargo.toml | 1 + demo/src/main.rs | 25 ++++++++++---- serve_zip/src/handler.rs | 69 +++++++++++++++++++++++++++++++++++++ serve_zip/src/lib.rs | 3 ++ serve_zip/src/serve_zip.rs | 70 ++++++++++++++++++++++++++++++++++++++ serve_zip/src/service.rs | 36 ++++++++++++++++++++ 7 files changed, 199 insertions(+), 6 deletions(-) create mode 100644 serve_zip/src/handler.rs create mode 100644 serve_zip/src/serve_zip.rs create mode 100644 serve_zip/src/service.rs diff --git a/Cargo.lock b/Cargo.lock index 13d4ea4..c410f35 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -262,6 +262,7 @@ version = "0.1.0" dependencies = [ "axum", "clap", + "serve_zip", "tokio", "tower-http", "tracing-subscriber", diff --git a/demo/Cargo.toml b/demo/Cargo.toml index b96d095..fa5b40e 100644 --- a/demo/Cargo.toml +++ b/demo/Cargo.toml @@ -9,3 +9,4 @@ clap = { version = "4.6.1", features = ["derive"] } tokio = { version = "1.52.3", features = ["full"] } tower-http = { version = "0.6.10", features = ["trace"] } tracing-subscriber = "0.3.23" +serve_zip = { path = "../serve_zip" } diff --git a/demo/src/main.rs b/demo/src/main.rs index 85ddfe9..592657f 100644 --- a/demo/src/main.rs +++ b/demo/src/main.rs @@ -1,5 +1,7 @@ use axum::{routing::get, Router}; use clap::Parser; +use serve_zip::serve_zip::ZipServe; +use serve_zip::source::ZipSource; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::path::PathBuf; use tokio::net::TcpListener; @@ -20,14 +22,25 @@ async fn main() { // TODO: Add a test to see if `path`, if it exists, is a path to a folder or to a zip file. If a // folder, opt for using serve_dir instead. - match &args.zip { - Some(path) => println!("Using external zip file: {}", path.display()), - None => { - println!("Using embedded zip file, if present. (TODO: Make this an error otherwise)") + let source = match &args.zip { + Some(path) => { + println!("Using external zip file: {}", path.display()); + ZipSource::File(path.clone()) } - } + None => { + println!("Using embedded zip file, if present. (TODO: Make this an error otherwise)"); + unimplemented!("Embedding isn't ready yet."); + } + }; + + let app = Router::new().fallback_service( + ZipServe::builder(source) + .append_index_html(true) + .build() + .await + .unwrap(), + ); - let app = Router::new().route("/check", get(|| async { "OK" })); let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), args.port); let listener = TcpListener::bind(addr) .await diff --git a/serve_zip/src/handler.rs b/serve_zip/src/handler.rs new file mode 100644 index 0000000..21452ef --- /dev/null +++ b/serve_zip/src/handler.rs @@ -0,0 +1,69 @@ +use std::convert::Infallible; +use std::sync::Arc; + +use axum::body::Body; +use http::{Method, Request, Response, StatusCode}; + +use crate::index::Entries; +use crate::serve_zip::ZipServeConfig; + +pub async fn serve( + data: Arc<[u8]>, + index: Arc, + config: ZipServeConfig, + req: Request, +) -> Result, Infallible> { + if req.method() != Method::GET && req.method() != Method::HEAD { + return Ok(Response::builder() + .status(StatusCode::METHOD_NOT_ALLOWED) + .header("allow", "GET, HEAD") + .body(Body::empty()) + .unwrap()); + } + + let path = req.uri().path(); + + match resolve_path(path, &index, &config) { + None => Ok(not_found()), + Some(entry_name) => serve_entry(data, config, entry_name, req.method().clone()).await, + } +} + +fn resolve_path(path: &str, index: &Entries, config: &ZipServeConfig) -> Option { + // Can't use the same trick we did in `index.rs`; need the path for more than just the lookup. + let normalized = path.strip_prefix('/').unwrap_or(path); + + if let Some(meta) = index.get(normalized) { + if meta.is_dir { + if config.append_index_html { + let index_path = format!("{}/index.html", normalized.trim_end_matches('/')); + if index.get(&index_path).is_some() { + return Some(index_path); + } + } + return None; + } + return Some(normalized.to_string()); + } + None +} + +fn not_found() -> Response { + Response::builder() + .status(StatusCode::NOT_FOUND) + .header("content-type", "text/plain; charset=utf-8") + .body(Body::from("Not Found")) + .unwrap() +} + +async fn serve_entry( + _data: Arc<[u8]>, + _config: ZipServeConfig, + _entry_name: String, + _method: Method, +) -> Result, Infallible> { + Ok(Response::builder() + .status(StatusCode::NOT_IMPLEMENTED) + .body(Body::empty()) + .unwrap()) +} diff --git a/serve_zip/src/lib.rs b/serve_zip/src/lib.rs index 7f9c127..4c281ff 100644 --- a/serve_zip/src/lib.rs +++ b/serve_zip/src/lib.rs @@ -1,2 +1,5 @@ +pub mod handler; pub mod index; +pub mod serve_zip; +pub mod service; pub mod source; diff --git a/serve_zip/src/serve_zip.rs b/serve_zip/src/serve_zip.rs new file mode 100644 index 0000000..c61575b --- /dev/null +++ b/serve_zip/src/serve_zip.rs @@ -0,0 +1,70 @@ +use std::sync::Arc; + +use crate::index::Entries; +use crate::source::ZipSource; + +#[derive(Clone, Debug)] +pub struct ZipServeConfig { + pub append_index_html: bool, + pub fallback_content_type: String, +} + +#[derive(Clone)] +pub struct ZipServe { + pub(crate) data: Arc<[u8]>, + pub(crate) index: Arc, + pub(crate) config: ZipServeConfig, +} + +pub struct ZipServeBuilder { + source: ZipSource, + config: ZipServeConfig, +} + +impl Default for ZipServeConfig { + fn default() -> Self { + ZipServeConfig { + append_index_html: false, + fallback_content_type: "application/octet-stream".to_string(), + } + } +} + +impl ZipServeBuilder { + pub fn new(source: ZipSource) -> Self { + ZipServeBuilder { + source, + config: ZipServeConfig::default(), + } + } + + pub fn append_index_html(mut self, val: bool) -> Self { + self.config.append_index_html = val; + self + } + + pub fn fallback_content_type(mut self, content_type: impl Into) -> Self { + self.config.fallback_content_type = content_type.into(); + self + } + + pub async fn build(self) -> Result> { + let data = self.source.load().await?; + let index = Entries::new(&data).await?; + Ok(ZipServe { + data, + index, + config: self.config, + }) + } +} + +impl ZipServe { + pub async fn new(source: ZipSource) -> Result> { + ZipServeBuilder::new(source).build().await + } + + pub fn builder(source: ZipSource) -> ZipServeBuilder { + ZipServeBuilder::new(source) + } +} diff --git a/serve_zip/src/service.rs b/serve_zip/src/service.rs new file mode 100644 index 0000000..3dd42ac --- /dev/null +++ b/serve_zip/src/service.rs @@ -0,0 +1,36 @@ +use std::convert::Infallible; +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use axum::body::Body; +use http::{Request, Response}; +use tower::Service; + +use crate::serve_zip::ZipServe; + +type ServeFuture = + Pin, Infallible>> + Send + 'static>>; + +impl Service> for ZipServe +where + ReqBody: Send + 'static, +{ + type Response = Response; + type Error = Infallible; + + type Future = ServeFuture; + + // We're always ready. The content's entirely internal at this point. + fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, req: Request) -> Self::Future { + let data = self.data.clone(); + let index = self.index.clone(); + let config = self.config.clone(); + + Box::pin(async move { crate::handler::serve(data, index, config, req).await }) + } +}