#[cfg(feature="serve_dir")] use std::path::{Path, PathBuf}; #[cfg(feature="serve_dir")] use mime::Mime; use anyhow::*; #[cfg(feature="serve_dir")] use tokio::{ fs::{self, File}, io, }; use crate::types::Response; use std::panic::{catch_unwind, AssertUnwindSafe}; use std::task::Poll; use futures_core::future::Future; use tokio::time; #[cfg(feature="serve_dir")] pub async fn serve_file>(path: P, mime: &Mime) -> Result { let path = path.as_ref(); let file = match File::open(path).await { Ok(file) => file, Err(err) => match err.kind() { std::io::ErrorKind::PermissionDenied => { warn!("Asked to serve {}, but permission denied by OS", path.display()); return Ok(Response::not_found()); }, _ => return warn_unexpected(err, path, line!()), } }; Ok(Response::success(mime, file)) } #[cfg(feature="serve_dir")] pub async fn serve_dir, P: AsRef>(dir: D, virtual_path: &[P]) -> Result { debug!("Dir: {}", dir.as_ref().display()); let dir = dir.as_ref(); let dir = match dir.canonicalize() { Ok(dir) => dir, Err(e) => { match e.kind() { std::io::ErrorKind::NotFound => { warn!("Path {} not found. Check your configuration.", dir.display()); return Response::server_error("Server incorrectly configured") }, std::io::ErrorKind::PermissionDenied => { warn!("Permission denied for {}. Check that the server has access.", dir.display()); return Response::server_error("Server incorrectly configured") }, _ => return warn_unexpected(e, dir, line!()), } }, }; let mut path = dir.to_path_buf(); for segment in virtual_path { path.push(segment); } let path = match path.canonicalize() { Ok(dir) => dir, Err(e) => { match e.kind() { std::io::ErrorKind::NotFound => return Ok(Response::not_found()), std::io::ErrorKind::PermissionDenied => { // Runs when asked to serve a file in a restricted dir // i.e. not /noaccess, but /noaccess/file warn!("Asked to serve {}, but permission denied by OS", path.display()); return Ok(Response::not_found()); }, _ => return warn_unexpected(e, path.as_ref(), line!()), } }, }; if !path.starts_with(&dir) { return Ok(Response::not_found()); } if !path.is_dir() { let mime = guess_mime_from_path(&path); return serve_file(path, &mime).await; } serve_dir_listing(path, virtual_path).await } #[cfg(feature="serve_dir")] async fn serve_dir_listing, B: AsRef>(path: P, virtual_path: &[B]) -> Result { let mut dir = match fs::read_dir(path.as_ref()).await { Ok(dir) => dir, Err(err) => match err.kind() { io::ErrorKind::NotFound => return Ok(Response::not_found()), std::io::ErrorKind::PermissionDenied => { warn!("Asked to serve {}, but permission denied by OS", path.as_ref().display()); return Ok(Response::not_found()); }, _ => return warn_unexpected(err, path.as_ref(), line!()), } }; let breadcrumbs: PathBuf = virtual_path.iter().collect(); let mut document = gemtext::Builder::new(); document = document.heading(1, format!("Index of /{}", breadcrumbs.display())) .blank_line(); if virtual_path.get(0).map(<_>::as_ref) != Some(Path::new("")) { document = document.link("..", Some("📁 ../")); } while let Some(entry) = dir.next_entry().await.context("Failed to list directory")? { let file_name = entry.file_name(); let file_name = file_name.to_string_lossy(); let is_dir = entry.file_type().await .with_context(|| format!("Failed to get file type of `{}`", entry.path().display()))? .is_dir(); let trailing_slash = if is_dir { "/" } else { "" }; let uri = format!("./{}{}", file_name, trailing_slash); document = document.link(uri.as_str(), Some(format!("{icon} {name}{trailing_slash}", icon = if is_dir { '📁' } else { '📄' }, name = file_name, trailing_slash = trailing_slash ))); } Ok(document.into()) } #[cfg(feature="serve_dir")] pub fn guess_mime_from_path>(path: P) -> Mime { let path = path.as_ref(); let extension = path.extension().and_then(|s| s.to_str()); let extension = match extension { Some(extension) => extension, None => return mime::APPLICATION_OCTET_STREAM, }; if let "gemini" | "gmi" = extension { return crate::GEMINI_MIME.clone(); } mime_guess::from_ext(extension).first_or_octet_stream() } #[cfg(feature="serve_dir")] /// Print a warning to the log asking to file an issue and respond with "Unexpected Error" pub (crate) fn warn_unexpected(err: impl std::fmt::Debug, path: &Path, line: u32) -> Result { warn!( concat!( "Unexpected error serving path {} at util.rs:{}, please report to ", env!("CARGO_PKG_REPOSITORY"), "/issues: {:?}", ), path.display(), line, err ); Response::server_error("Unexpected error") } /// A convenience trait alias for `AsRef + Into`, /// most commonly used to accept `&str` or `String`: /// /// `Cowy` ⇔ `AsRef + Into` pub trait Cowy where Self: AsRef + Into, T: ToOwned + ?Sized, {} impl Cowy for C where C: AsRef + Into, T: ToOwned + ?Sized, {} /// A utility for catching unwinds on Futures. /// /// This is adapted from the futures-rs CatchUnwind, in an effort to reduce the large /// amount of dependencies tied into the feature that provides this simple struct. #[must_use = "futures do nothing unless polled"] pub (crate) struct HandlerCatchUnwind { future: AssertUnwindSafe, } impl HandlerCatchUnwind { pub(super) fn new(future: AssertUnwindSafe) -> Self { Self { future } } } impl Future for HandlerCatchUnwind { type Output = Result, Box>; fn poll( mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context ) -> Poll { match catch_unwind(AssertUnwindSafe(|| self.future.as_mut().poll(cx))) { Ok(res) => res.map(Ok), Err(e) => Poll::Ready(Err(e)) } } } pub(crate) async fn opt_timeout(duration: Option, future: impl Future) -> Result { match duration { Some(duration) => time::timeout(duration, future).await, None => Ok(future.await), } }