use crate::{config::FileserverConfig, ServiceError}; use bytes::{Bytes, BytesMut}; use futures_util::{future, future::Either, ready, stream, FutureExt, Stream, StreamExt}; use http_body_util::{combinators::BoxBody, BodyExt, StreamBody}; use humansize::FormatSizeOptions; use hyper::{ body::{Frame, Incoming}, header::{CONTENT_TYPE, LOCATION}, http::HeaderValue, Request, Response, StatusCode, }; use log::debug; use markup::Render; use std::{fs::Metadata, io, path::Path, pin::Pin, task::Poll}; use tokio::{ fs::{read_to_string, File}, io::AsyncSeekExt, }; use tokio_util::io::poll_read_buf; pub async fn serve_files( req: Request, config: &FileserverConfig, ) -> Result>, ServiceError> { let rpath = req.uri().path(); let mut path = config.root.clone(); for seg in rpath.split("/") { if seg == "" || seg == ".." { continue; // not ideal } path.push(seg) } if !path.exists() { return Err(ServiceError::NotFound); } if path.is_dir() { if !config.index { return Err(ServiceError::NotFound); } if !rpath.ends_with("/") { let mut r = Response::new(String::new()); *r.status_mut() = StatusCode::FOUND; r.headers_mut().insert( LOCATION, HeaderValue::from_str(&format!("{}/", rpath)).map_err(|_| ServiceError::Other)?, ); return Ok(r.map(|b| b.map_err(|e| match e {}).boxed())); } return index(&path, rpath.to_string()).await.map(|s| { let mut r = Response::new(s); r.headers_mut() .insert(CONTENT_TYPE, HeaderValue::from_static("text/html")); r.map(|b| b.map_err(|e| match e {}).boxed()) }); } let file = File::open(path.clone()).await?; let mut r = Response::new(BoxBody::new(StreamBody::new( StreamBody::new(file_stream(file, 4096, (0, u64::MAX))) .map(|e| e.map(|e| Frame::data(e)).map_err(ServiceError::Io)), ))); r.headers_mut().insert( CONTENT_TYPE, HeaderValue::from_str( // no allocation possible here? &mime_guess::from_path(path) .first() .map(|m| m.to_string()) .unwrap_or("text/plain".to_string()), ) .unwrap(), ); Ok(r) } // Adapted from warp (https://github.com/seanmonstar/warp/blob/master/src/filters/fs.rs). Thanks! fn file_stream( mut file: File, buf_size: usize, (start, end): (u64, u64), ) -> impl Stream> + Send { use std::io::SeekFrom; let seek = async move { if start != 0 { file.seek(SeekFrom::Start(start)).await?; } Ok(file) }; seek.into_stream() .map(move |result| { let mut buf = BytesMut::new(); let mut len = end - start; let mut f = match result { Ok(f) => f, Err(f) => return Either::Left(stream::once(future::err(f))), }; Either::Right(stream::poll_fn(move |cx| { if len == 0 { return Poll::Ready(None); } reserve_at_least(&mut buf, buf_size); let n = match ready!(poll_read_buf(Pin::new(&mut f), cx, &mut buf)) { Ok(n) => n as u64, Err(err) => { debug!("file read error: {}", err); return Poll::Ready(Some(Err(err))); } }; if n == 0 { debug!("file read found EOF before expected length"); return Poll::Ready(None); } let mut chunk = buf.split().freeze(); if n > len { chunk = chunk.split_to(len as usize); len = 0; } else { len -= n; } Poll::Ready(Some(Ok(chunk))) })) }) .flatten() } fn reserve_at_least(buf: &mut BytesMut, cap: usize) { if buf.capacity() - buf.len() < cap { buf.reserve(cap); } } async fn index(path: &Path, rpath: String) -> Result { let files = path .read_dir()? .map(|e| e.and_then(|e| Ok((e.file_name().into_string().unwrap(), e.metadata()?)))) .collect::, _>>()?; if let Ok(indexhtml) = read_to_string(path.join("index.html")).await { Ok(indexhtml) } else { let banner = read_to_string(path.join("index.banner.html")).await.ok(); let mut s = String::new(); IndexTemplate { files, banner, path: rpath, } .render(&mut s) .unwrap(); Ok(s) } } markup::define! { IndexTemplate(path: String, banner: Option, files: Vec<(String, Metadata)>) { @markup::doctype() html { head { title { "Index of " @path } } body { @if let Some(banner) = banner { @markup::raw(banner) } else { h1 { "Index of " @path } } hr; table { @if path != "/" { tr { td { b { a[href=".."] { "../" } } } } } @for (name, meta) in files { tr { td { a[href=name] { @name @if meta.file_type().is_dir() { "/" } } } td { @if meta.file_type().is_dir() { i { "directory" } } else { @humansize::format_size(meta.len(), FormatSizeOptions::default()) } } } } } hr; footer { sub { "served by " a[href="https://codeberg.org/metamuffin/gnix"] { "gnix" } } } } } } }