summaryrefslogtreecommitdiff
path: root/src/files.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/files.rs')
-rw-r--r--src/files.rs193
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" } }
+ }
+ }
+ }
+}