use std::path::{Path, PathBuf}; use tracing::{error, instrument}; use super::{Content, Dir, EditPath, Entries, Query, VPath, VPathBuf}; #[derive(Debug, Clone)] pub struct PhysicalDir { root: PathBuf, } impl PhysicalDir { pub fn new(root: PathBuf) -> Self { Self { root } } fn entries(&self, vpath: &VPath) -> Vec { let physical = self.root.join(physical_path(vpath)); if !physical.is_dir() { return vec![]; } match std::fs::read_dir(physical) { Ok(read_dir) => read_dir .filter_map(|entry| { entry .inspect_err(|err| { error!( "{self:?} error while reading entries: {err:?}", ) }) .ok() .and_then(|entry| { let path = entry.path(); let path_str = match path.strip_prefix(&self.root).unwrap_or(&path).to_str() { Some(p) => p, None => { error!("{self:?} entry {path:?} has invalid UTF-8 (while reading vpath {vpath:?})"); return None; }, }; let vpath_buf = VPathBuf::try_new(path_str.replace('\\', "/")) .inspect_err(|err| { error!("{self:?} error with vpath for {path_str:?}: {err:?}"); }) .ok()?; Some(vpath_buf) }) }) .collect(), Err(err) => { error!( "{self:?} cannot read vpath {vpath:?}: {err:?}", ); vec![] } } } #[instrument("PhysicalDir::content", skip(self))] fn content(&self, path: &VPath) -> Option { let physical_path = self.root.join(physical_path(path)); std::fs::read(&physical_path) .inspect_err(|err| error!("{self:?} cannot read file at vpath {path:?} / physical {physical_path:?}: {err:?}",)) .ok() .map(|bytes| { Content::new( path.extension() .and_then(guess_content_type) .unwrap_or("text/plain"), bytes, ) }) } fn edit_path(&self, path: &VPath) -> EditPath { EditPath { path: self.root.join(physical_path(path)), } } } impl Dir for PhysicalDir { fn query(&self, path: &VPath, query: &mut Query) { query.provide(|| Entries(self.entries(path))); query.try_provide(|| self.content(path)); query.provide(|| self.edit_path(path)); } } fn physical_path(path: &VPath) -> &Path { Path::new(path.as_str()) } fn guess_content_type(extension: &str) -> Option<&'static str> { match extension { "html" => Some("text/html"), "js" => Some("text/javascript"), "css" => Some("text/css"), "woff" => Some("font/woff2"), "svg" => Some("image/svg+xml"), "atom" => Some("application/atom+xml"), "png" => Some("image/png"), "webp" => Some("image/webp"), "jpg" | "jpeg" => Some("image/jpeg"), _ => None, } }