treehouse/src/vfs/physical.rs

108 lines
3.5 KiB
Rust

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<VPathBuf> {
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<Content> {
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,
}
}