diff options
Diffstat (limited to 'src/files.rs')
-rw-r--r-- | src/files.rs | 193 |
1 files changed, 193 insertions, 0 deletions
diff --git a/src/files.rs b/src/files.rs new file mode 100644 index 0000000..e092994 --- /dev/null +++ b/src/files.rs @@ -0,0 +1,193 @@ +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<Incoming>, + config: &FileserverConfig, +) -> Result<hyper::Response<BoxBody<Bytes, ServiceError>>, 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<Item = Result<Bytes, io::Error>> + 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<String, ServiceError> { + let files = path + .read_dir()? + .map(|e| e.and_then(|e| Ok((e.file_name().into_string().unwrap(), e.metadata()?)))) + .collect::<Result<Vec<_>, _>>()?; + 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<String>, 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 { + @for (name, meta) in files { tr { + td { @if meta.file_type().is_dir() { "(dir)" } else { "(file)" } } + td { a[href=name] { @name } } + td { @humansize::format_size(meta.len(), FormatSizeOptions::default()) } + } } + } + hr; + footer { sub { "served by gnix" } } + } + } + } +} |