//! Utilities for serving a file or directory //! //! ⚠️ Docs still under construction & API not yet stable ⚠️ #![allow(missing_docs)] #[cfg(feature="serve_dir")] use std::path::{Path, PathBuf}; 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: &str) -> Response { 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 Response::not_found(); }, _ => return warn_unexpected(err, path, line!()), } }; Response::success(mime, file) } #[cfg(feature="serve_dir")] pub async fn serve_dir, P: AsRef>(dir: D, virtual_path: &[P]) -> Response { 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::temporary_failure("Server incorrectly configured") }, std::io::ErrorKind::PermissionDenied => { warn!("Permission denied for {}. Check that the server has access.", dir.display()); return Response::temporary_failure("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 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 Response::not_found(); }, _ => return warn_unexpected(e, path.as_ref(), line!()), } }, }; if !path.starts_with(&dir) { return 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]) -> Response { let mut dir = match fs::read_dir(path.as_ref()).await { Ok(dir) => dir, Err(err) => match err.kind() { io::ErrorKind::NotFound => return Response::not_found(), std::io::ErrorKind::PermissionDenied => { warn!("Asked to serve {}, but permission denied by OS", path.as_ref().display()); return 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.expect("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.unwrap().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 )); } document.into() } #[cfg(feature="serve_dir")] pub fn guess_mime_from_path>(path: P) -> &'static str { let path = path.as_ref(); let extension = path.extension().and_then(|s| s.to_str()); let extension = match extension { Some(extension) => extension, None => return "application/octet-stream" }; if let "gemini" | "gmi" = extension { return "text/gemini"; } mime_guess::from_ext(extension).first_raw().unwrap_or("application/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) -> Response { warn!( concat!( "Unexpected error serving path {} at util.rs:{}, please report to ", env!("CARGO_PKG_REPOSITORY"), "/issues: {:?}", ), path.display(), line, err ); Response::temporary_failure("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, {}