use std::net::SocketAddr;
use std::path::Path;
use axum::extract::{Request, State};
use axum::middleware::{self, Next};
use axum::response::Response;
use tower_http::services::ServeDir;
use url::Url;
pub async fn serve_file(file_path: impl AsRef<Path>) -> Url {
let file_path = file_path.as_ref();
let file_size = std::fs::metadata(file_path).unwrap().len();
serve(file_path, |router| {
router.layer(middleware::from_fn_with_state(
file_size,
clamp_suffix_range,
))
})
.await
}
pub async fn serve_file_no_ranges(file_path: impl AsRef<Path>) -> Url {
serve(file_path.as_ref(), |router| {
router.layer(middleware::from_fn(strip_range))
})
.await
}
pub async fn serve_file_416_suffix(file_path: impl AsRef<Path>) -> Url {
serve(file_path.as_ref(), |router| {
router.layer(middleware::from_fn(reject_suffix_range))
})
.await
}
async fn serve(file_path: &Path, layer: impl FnOnce(axum::Router) -> axum::Router) -> Url {
let file_name = file_path.file_name().unwrap().to_string_lossy().to_string();
let dir = file_path.parent().unwrap();
let app = layer(axum::Router::new().fallback_service(ServeDir::new(dir)));
let addr = SocketAddr::new([127, 0, 0, 1].into(), 0);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();
});
format!("http://{}:{}/{file_name}", addr.ip(), addr.port())
.parse()
.unwrap()
}
async fn reject_suffix_range(req: Request, next: Next) -> Response {
let is_suffix = req
.headers()
.get(http::header::RANGE)
.and_then(|value| value.to_str().ok())
.is_some_and(|range| range.starts_with("bytes=-"));
if is_suffix {
return Response::builder()
.status(http::StatusCode::RANGE_NOT_SATISFIABLE)
.body(axum::body::Body::empty())
.unwrap();
}
next.run(req).await
}
async fn strip_range(mut req: Request, next: Next) -> Response {
req.headers_mut().remove(http::header::RANGE);
let mut response = next.run(req).await;
response.headers_mut().remove(http::header::ACCEPT_RANGES);
response
}
async fn clamp_suffix_range(
State(file_size): State<u64>,
mut req: Request,
next: Next,
) -> Response {
if let Some(range_val) = req.headers().get(http::header::RANGE)
&& let Ok(range_str) = range_val.to_str()
&& let Some(suffix) = range_str.strip_prefix("bytes=-")
&& let Ok(n) = suffix.parse::<u64>()
&& n > file_size
{
req.headers_mut().insert(
http::header::RANGE,
format!("bytes=0-{}", file_size - 1).parse().unwrap(),
);
}
next.run(req).await
}