Well, it's serving 'I know it exists, but I can't show it to you.'

This commit is contained in:
Elf M. Sternberg 2026-05-18 17:33:07 -07:00
parent e85da1cf34
commit 5245420073
7 changed files with 199 additions and 6 deletions

1
Cargo.lock generated
View File

@ -262,6 +262,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"axum", "axum",
"clap", "clap",
"serve_zip",
"tokio", "tokio",
"tower-http", "tower-http",
"tracing-subscriber", "tracing-subscriber",

View File

@ -9,3 +9,4 @@ clap = { version = "4.6.1", features = ["derive"] }
tokio = { version = "1.52.3", features = ["full"] } tokio = { version = "1.52.3", features = ["full"] }
tower-http = { version = "0.6.10", features = ["trace"] } tower-http = { version = "0.6.10", features = ["trace"] }
tracing-subscriber = "0.3.23" tracing-subscriber = "0.3.23"
serve_zip = { path = "../serve_zip" }

View File

@ -1,5 +1,7 @@
use axum::{routing::get, Router}; use axum::{routing::get, Router};
use clap::Parser; use clap::Parser;
use serve_zip::serve_zip::ZipServe;
use serve_zip::source::ZipSource;
use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::path::PathBuf; use std::path::PathBuf;
use tokio::net::TcpListener; 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 // 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. // folder, opt for using serve_dir instead.
match &args.zip { let source = match &args.zip {
Some(path) => println!("Using external zip file: {}", path.display()), Some(path) => {
None => { println!("Using external zip file: {}", path.display());
println!("Using embedded zip file, if present. (TODO: Make this an error otherwise)") 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 addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), args.port);
let listener = TcpListener::bind(addr) let listener = TcpListener::bind(addr)
.await .await

69
serve_zip/src/handler.rs Normal file
View File

@ -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<B>(
data: Arc<[u8]>,
index: Arc<Entries>,
config: ZipServeConfig,
req: Request<B>,
) -> Result<Response<Body>, 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<String> {
// 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<Body> {
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<Response<Body>, Infallible> {
Ok(Response::builder()
.status(StatusCode::NOT_IMPLEMENTED)
.body(Body::empty())
.unwrap())
}

View File

@ -1,2 +1,5 @@
pub mod handler;
pub mod index; pub mod index;
pub mod serve_zip;
pub mod service;
pub mod source; pub mod source;

View File

@ -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<Entries>,
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<String>) -> Self {
self.config.fallback_content_type = content_type.into();
self
}
pub async fn build(self) -> Result<ZipServe, Box<dyn std::error::Error + Send + Sync>> {
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<Self, Box<dyn std::error::Error + Send + Sync>> {
ZipServeBuilder::new(source).build().await
}
pub fn builder(source: ZipSource) -> ZipServeBuilder {
ZipServeBuilder::new(source)
}
}

36
serve_zip/src/service.rs Normal file
View File

@ -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<Box<dyn Future<Output = Result<Response<Body>, Infallible>> + Send + 'static>>;
impl<ReqBody> Service<Request<ReqBody>> for ZipServe
where
ReqBody: Send + 'static,
{
type Response = Response<Body>;
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<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: Request<ReqBody>) -> 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 })
}
}