#[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, }; #[cfg(feature="serve_dir")] use crate::types::{Document, document::HeadingLevel::*}; #[cfg(feature="serve_dir")] use crate::types::Response; use std::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 = Document::new(); document.add_heading(H1, format!("Index of /{}", breadcrumbs.display())); document.add_blank_line(); if virtual_path.get(0).map(<_>::as_ref) != Some(Path::new("")) { document.add_link("..", "📁 ../"); } 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.add_link(uri.as_str(), 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, {} 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), } }