change vfs API to something akin to the deleted std provider API

this gives us a _much_ easier time with composing file systems, since you can call `query` on the inner file system and all the magic will be done for you
the overhead is technically slightly higher, but seems to be drowned out by other more expensive stuff; i couldn't measure a difference in my traces
This commit is contained in:
liquidex 2024-11-29 20:03:32 +01:00
parent 5b6d637f44
commit 600651ec16
29 changed files with 418 additions and 611 deletions

View file

@ -8,7 +8,7 @@ use treehouse_format::ast::Branch;
use crate::{ use crate::{
parse::{self, parse_toml_with_diagnostics, parse_tree_with_diagnostics}, parse::{self, parse_toml_with_diagnostics, parse_tree_with_diagnostics},
state::{report_diagnostics, FileId, Source, Treehouse}, state::{report_diagnostics, FileId, Source, Treehouse},
vfs::{self, Dir, Edit, VPath}, vfs::{self, Content, Dir, Edit, EditPath, VPath},
}; };
use super::{FixAllArgs, FixArgs}; use super::{FixAllArgs, FixArgs};
@ -138,17 +138,15 @@ pub fn fix_file_cli(fix_args: FixArgs, root: &dyn Dir) -> anyhow::Result<Edit> {
let file = if &*fix_args.file == VPath::new("-") { let file = if &*fix_args.file == VPath::new("-") {
std::io::read_to_string(std::io::stdin().lock()).context("cannot read file from stdin")? std::io::read_to_string(std::io::stdin().lock()).context("cannot read file from stdin")?
} else { } else {
String::from_utf8( vfs::query::<Content>(root, &fix_args.file)
root.content(&fix_args.file) .ok_or_else(|| anyhow!("cannot read file to fix"))?
.ok_or_else(|| anyhow!("cannot read file to fix"))?, .string()?
)
.context("input file has invalid UTF-8")?
}; };
let mut treehouse = Treehouse::new(); let mut treehouse = Treehouse::new();
let mut diagnostics = vec![]; let mut diagnostics = vec![];
let file_id = treehouse.add_file(fix_args.file.clone(), Source::Other(file)); let file_id = treehouse.add_file(fix_args.file.clone(), Source::Other(file));
let edit_path = root.edit_path(&fix_args.file).ok_or_else(|| { let edit_path = vfs::query::<EditPath>(root, &fix_args.file).ok_or_else(|| {
anyhow!( anyhow!(
"{} is not an editable file (perhaps it is not in a persistent path?)", "{} is not an editable file (perhaps it is not in a persistent path?)",
fix_args.file fix_args.file
@ -161,9 +159,10 @@ pub fn fix_file_cli(fix_args: FixArgs, root: &dyn Dir) -> anyhow::Result<Edit> {
// Try to write the backup first. If writing that fails, bail out without overwriting // Try to write the backup first. If writing that fails, bail out without overwriting
// the source file. // the source file.
if let Some(backup_path) = fix_args.backup { if let Some(backup_path) = fix_args.backup {
let backup_edit_path = root.edit_path(&backup_path).ok_or_else(|| { let backup_edit_path =
anyhow!("backup file {backup_path} is not an editable file") vfs::query::<EditPath>(root, &backup_path).ok_or_else(|| {
})?; anyhow!("backup file {backup_path} is not an editable file")
})?;
Edit::Seq(vec![ Edit::Seq(vec![
Edit::Write( Edit::Write(
backup_edit_path, backup_edit_path,
@ -190,7 +189,7 @@ pub fn fix_all_cli(fix_all_args: FixAllArgs, dir: &dyn Dir) -> anyhow::Result<Ed
fn fix_one(dir: &dyn Dir, path: &VPath) -> anyhow::Result<Edit> { fn fix_one(dir: &dyn Dir, path: &VPath) -> anyhow::Result<Edit> {
if path.extension() == Some("tree") { if path.extension() == Some("tree") {
let Some(content) = dir.content(path) else { let Some(content) = vfs::query::<Content>(dir, path).map(Content::bytes) else {
return Ok(Edit::NoOp); return Ok(Edit::NoOp);
}; };
let content = String::from_utf8(content).context("file is not valid UTF-8")?; let content = String::from_utf8(content).context("file is not valid UTF-8")?;
@ -198,7 +197,7 @@ pub fn fix_all_cli(fix_all_args: FixAllArgs, dir: &dyn Dir) -> anyhow::Result<Ed
let mut treehouse = Treehouse::new(); let mut treehouse = Treehouse::new();
let mut diagnostics = vec![]; let mut diagnostics = vec![];
let file_id = treehouse.add_file(path.to_owned(), Source::Other(content)); let file_id = treehouse.add_file(path.to_owned(), Source::Other(content));
let edit_path = dir.edit_path(path).context("path is not editable")?; let edit_path = vfs::query::<EditPath>(dir, path).context("path is not editable")?;
if let Ok(fixed) = fix_file(&mut treehouse, &mut diagnostics, file_id) { if let Ok(fixed) = fix_file(&mut treehouse, &mut diagnostics, file_id) {
if fixed != treehouse.source(file_id).input() { if fixed != treehouse.source(file_id).input() {

View file

@ -73,7 +73,7 @@ struct VfsQuery {
#[instrument(skip(state))] #[instrument(skip(state))]
async fn get_static_file(path: &str, query: &VfsQuery, state: &Server) -> Option<Response> { async fn get_static_file(path: &str, query: &VfsQuery, state: &Server) -> Option<Response> {
let vpath = VPath::try_new(path).ok()?; let vpath = VPath::try_new(path).ok()?;
let content = state.target.content(vpath).await?; let content = state.target.content(vpath).await.map(|c| c.bytes())?;
let mut response = content.into_response(); let mut response = content.into_response();
if let Some(content_type) = vpath.extension().and_then(get_content_type) { if let Some(content_type) = vpath.extension().and_then(get_content_type) {
@ -108,7 +108,7 @@ async fn vfs_entry(
async fn system_page(target: &AsyncDir, path: &VPath, status_code: StatusCode) -> Response { async fn system_page(target: &AsyncDir, path: &VPath, status_code: StatusCode) -> Response {
if let Some(content) = target.content(path).await { if let Some(content) = target.content(path).await {
(status_code, Html(content)).into_response() (status_code, Html(content.bytes())).into_response()
} else { } else {
( (
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
@ -152,7 +152,7 @@ async fn branch(RawQuery(named_id): RawQuery, State(state): State<Arc<Server>>)
.target .target
.content(tree_path) .content(tree_path)
.await .await
.and_then(|s| String::from_utf8(s).ok()) .and_then(|c| c.string().ok())
{ {
let branch_markup = input[branch.content.clone()].trim(); let branch_markup = input[branch.content.clone()].trim();
let mut per_page_metadata = let mut per_page_metadata =

View file

@ -5,7 +5,7 @@ use treehouse_format::ast::{Branch, Roots};
use crate::{ use crate::{
parse::parse_tree_with_diagnostics, parse::parse_tree_with_diagnostics,
state::{report_diagnostics, Source, Treehouse}, state::{report_diagnostics, Source, Treehouse},
vfs::{self, Dir, VPath}, vfs::{self, Content, Dir, VPath},
}; };
use super::WcArgs; use super::WcArgs;
@ -43,9 +43,8 @@ pub fn wc_cli(content_dir: &dyn Dir, mut wc_args: WcArgs) -> anyhow::Result<()>
let mut total = 0; let mut total = 0;
for path in &wc_args.paths { for path in &wc_args.paths {
if let Some(content) = content_dir if let Some(content) =
.content(path) vfs::query::<Content>(content_dir, path).and_then(|b| b.string().ok())
.and_then(|b| String::from_utf8(b).ok())
{ {
let file_id = treehouse.add_file(path.clone(), Source::Other(content.clone())); let file_id = treehouse.add_file(path.clone(), Source::Other(content.clone()));
match parse_tree_with_diagnostics(file_id, &content) { match parse_tree_with_diagnostics(file_id, &content) {

View file

@ -14,7 +14,7 @@ use crate::{
Syntax, Syntax,
}, },
import_map::ImportRoot, import_map::ImportRoot,
vfs::{self, Dir, DynDir, ImageSize, VPath, VPathBuf}, vfs::{self, Content, Dir, DynDir, ImageSize, VPath, VPathBuf},
}; };
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
@ -167,7 +167,9 @@ impl Config {
} }
pub fn pic_size(&self, pics_dir: &dyn Dir, id: &str) -> Option<ImageSize> { pub fn pic_size(&self, pics_dir: &dyn Dir, id: &str) -> Option<ImageSize> {
self.pics.get(id).and_then(|path| pics_dir.image_size(path)) self.pics
.get(id)
.and_then(|path| vfs::query::<ImageSize>(pics_dir, path))
} }
/// Loads all syntax definition files. /// Loads all syntax definition files.
@ -188,12 +190,9 @@ impl Config {
.file_stem() .file_stem()
.expect("syntax file name should have a stem due to the .json extension"); .expect("syntax file name should have a stem due to the .json extension");
let result: Result<Syntax, _> = dir let result: Result<Syntax, _> = vfs::query::<Content>(&dir, path)
.content(path)
.ok_or_else(|| anyhow!("syntax .json is not a file")) .ok_or_else(|| anyhow!("syntax .json is not a file"))
.and_then(|b| { .and_then(|b| b.string().context("syntax .json contains invalid UTF-8"))
String::from_utf8(b).context("syntax .json contains invalid UTF-8")
})
.and_then(|s| { .and_then(|s| {
let _span = info_span!("Config::load_syntaxes::parse").entered(); let _span = info_span!("Config::load_syntaxes::parse").entered();
serde_json::from_str(&s).context("could not deserialize syntax file") serde_json::from_str(&s).context("could not deserialize syntax file")

View file

@ -19,8 +19,8 @@ use crate::{
fun::seasons::Season, fun::seasons::Season,
sources::Sources, sources::Sources,
vfs::{ vfs::{
self, Cd, ContentCache, Dir, DirEntry, DynDir, HtmlCanonicalize, MemDir, Overlay, ToDynDir, self, Cd, Content, ContentCache, Dir, DynDir, Entries, HtmlCanonicalize, MemDir, Overlay,
VPath, VPathBuf, ToDynDir, VPath, VPathBuf,
}, },
}; };
@ -46,6 +46,36 @@ impl<'a> BaseTemplateData<'a> {
} }
} }
fn create_handlebars(site: &str, static_: DynDir) -> Handlebars<'static> {
let mut handlebars = Handlebars::new();
handlebars_helper!(cat: |a: String, b: String| a + &b);
handlebars.register_helper("cat", Box::new(cat));
handlebars.register_helper("asset", Box::new(DirHelper::new(site, static_.clone())));
handlebars.register_helper(
"include_static",
Box::new(IncludeStaticHelper::new(static_)),
);
handlebars
}
#[instrument(skip(handlebars))]
fn load_templates(handlebars: &mut Handlebars, dir: &dyn Dir) {
vfs::walk_dir_rec(dir, VPath::ROOT, &mut |path| {
if path.extension() == Some("hbs") {
if let Some(content) = vfs::query::<Content>(dir, path).and_then(|c| c.string().ok()) {
let _span = info_span!("register_template", ?path).entered();
if let Err(err) = handlebars.register_template_string(path.as_str(), content) {
error!("in template: {err}");
}
}
}
ControlFlow::Continue(())
});
}
struct TreehouseDir { struct TreehouseDir {
dirs: Arc<Dirs>, dirs: Arc<Dirs>,
sources: Arc<Sources>, sources: Arc<Sources>,
@ -67,41 +97,9 @@ impl TreehouseDir {
dir_index, dir_index,
} }
} }
}
fn create_handlebars(site: &str, static_: DynDir) -> Handlebars<'static> {
let mut handlebars = Handlebars::new();
handlebars_helper!(cat: |a: String, b: String| a + &b);
handlebars.register_helper("cat", Box::new(cat));
handlebars.register_helper("asset", Box::new(DirHelper::new(site, static_.clone())));
handlebars.register_helper(
"include_static",
Box::new(IncludeStaticHelper::new(static_)),
);
handlebars
}
#[instrument(skip(handlebars))]
fn load_templates(handlebars: &mut Handlebars, dir: &dyn Dir) {
vfs::walk_dir_rec(dir, VPath::ROOT, &mut |path| {
if path.extension() == Some("hbs") {
if let Some(content) = dir.content(path).and_then(|b| String::from_utf8(b).ok()) {
let _span = info_span!("register_template", ?path).entered();
if let Err(err) = handlebars.register_template_string(path.as_str(), content) {
error!("in template: {err}");
}
}
}
ControlFlow::Continue(())
});
}
impl Dir for TreehouseDir {
#[instrument("TreehouseDir::dir", skip(self))] #[instrument("TreehouseDir::dir", skip(self))]
fn dir(&self, path: &VPath) -> Vec<DirEntry> { fn dir(&self, path: &VPath) -> Vec<VPathBuf> {
// NOTE: This does not include simple templates, because that's not really needed right now. // NOTE: This does not include simple templates, because that's not really needed right now.
let mut index = &self.dir_index; let mut index = &self.dir_index;
@ -118,14 +116,12 @@ impl Dir for TreehouseDir {
index index
.children .children
.values() .values()
.map(|child| DirEntry { .map(|child| child.full_path.clone())
path: child.full_path.clone(),
})
.collect() .collect()
} }
#[instrument("TreehouseDir::content", skip(self))] #[instrument("TreehouseDir::content", skip(self))]
fn content(&self, path: &VPath) -> Option<Vec<u8>> { fn content(&self, path: &VPath) -> Option<Content> {
let path = if path.is_root() { let path = if path.is_root() {
VPath::new_const("index") VPath::new_const("index")
} else { } else {
@ -137,28 +133,34 @@ impl Dir for TreehouseDir {
.files_by_tree_path .files_by_tree_path
.get(path) .get(path)
.map(|&file_id| { .map(|&file_id| {
tree::generate_or_error(&self.sources, &self.dirs, &self.handlebars, file_id).into() Content::new(
tree::generate_or_error(&self.sources, &self.dirs, &self.handlebars, file_id)
.into(),
)
}) })
.or_else(|| { .or_else(|| {
if path.file_name().is_some_and(|s| !s.starts_with('_')) { if path.file_name().is_some_and(|s| !s.starts_with('_')) {
let template_name = path.with_extension("hbs"); let template_name = path.with_extension("hbs");
if self.handlebars.has_template(template_name.as_str()) { if self.handlebars.has_template(template_name.as_str()) {
return Some( return Some(Content::new(
simple_template::generate_or_error( simple_template::generate_or_error(
&self.sources, &self.sources,
&self.handlebars, &self.handlebars,
template_name.as_str(), template_name.as_str(),
) )
.into(), .into(),
); ));
} }
} }
None None
}) })
} }
}
fn content_version(&self, _path: &VPath) -> Option<String> { impl Dir for TreehouseDir {
None fn query(&self, path: &VPath, query: &mut vfs::Query) {
query.provide(|| Entries(self.dir(path)));
query.try_provide(|| self.content(path));
} }
} }

View file

@ -13,7 +13,7 @@ use crate::{
sources::Sources, sources::Sources,
state::FileId, state::FileId,
tree::SemaBranchId, tree::SemaBranchId,
vfs::{Dir, DirEntry, VPath, VPathBuf}, vfs::{self, Content, Dir, Entries, VPath, VPathBuf},
}; };
use super::BaseTemplateData; use super::BaseTemplateData;
@ -36,25 +36,21 @@ impl FeedDir {
handlebars, handlebars,
} }
} }
}
impl Dir for FeedDir { fn entries(&self, path: &VPath) -> Vec<VPathBuf> {
fn dir(&self, path: &VPath) -> Vec<DirEntry> {
if path == VPath::ROOT { if path == VPath::ROOT {
self.sources self.sources
.treehouse .treehouse
.feeds_by_name .feeds_by_name
.keys() .keys()
.map(|name| DirEntry { .map(|name| VPathBuf::new(format!("{name}.atom")))
path: VPathBuf::new(format!("{name}.atom")),
})
.collect() .collect()
} else { } else {
vec![] vec![]
} }
} }
fn content(&self, path: &VPath) -> Option<Vec<u8>> { fn content(&self, path: &VPath) -> Option<Content> {
info!("{path}"); info!("{path}");
if path.extension() == Some("atom") { if path.extension() == Some("atom") {
let feed_name = path.with_extension("").to_string(); let feed_name = path.with_extension("").to_string();
@ -63,15 +59,21 @@ impl Dir for FeedDir {
.feeds_by_name .feeds_by_name
.get(&feed_name) .get(&feed_name)
.map(|file_id| { .map(|file_id| {
generate_or_error(&self.sources, &self.dirs, &self.handlebars, *file_id).into() Content::new(
generate_or_error(&self.sources, &self.dirs, &self.handlebars, *file_id)
.into(),
)
}) })
} else { } else {
None None
} }
} }
}
fn content_version(&self, _path: &VPath) -> Option<String> { impl Dir for FeedDir {
None fn query(&self, path: &VPath, query: &mut vfs::Query) {
query.provide(|| Entries(self.entries(path)));
query.try_provide(|| self.content(path));
} }
} }

View file

@ -1,7 +1,7 @@
use handlebars::{Context, Handlebars, Helper, HelperDef, RenderContext, RenderError, ScopedJson}; use handlebars::{Context, Handlebars, Helper, HelperDef, RenderContext, RenderError, ScopedJson};
use serde_json::Value; use serde_json::Value;
use crate::vfs::{DynDir, VPath}; use crate::vfs::{self, Content, DynDir, VPath};
pub struct IncludeStaticHelper { pub struct IncludeStaticHelper {
dir: DynDir, dir: DynDir,
@ -23,13 +23,11 @@ impl HelperDef for IncludeStaticHelper {
) -> Result<ScopedJson<'reg, 'rc>, RenderError> { ) -> Result<ScopedJson<'reg, 'rc>, RenderError> {
if let Some(path) = h.param(0).and_then(|v| v.value().as_str()) { if let Some(path) = h.param(0).and_then(|v| v.value().as_str()) {
let vpath = VPath::try_new(path).map_err(|e| RenderError::new(e.to_string()))?; let vpath = VPath::try_new(path).map_err(|e| RenderError::new(e.to_string()))?;
let url = String::from_utf8( let content = vfs::query::<Content>(&self.dir, vpath)
self.dir .ok_or_else(|| RenderError::new("file does not exist"))?
.content(vpath) .string()
.ok_or_else(|| RenderError::new("file does not exist"))?, .map_err(|_| RenderError::new("included file does not contain UTF-8 text"))?;
) Ok(ScopedJson::Derived(Value::String(content)))
.map_err(|_| RenderError::new("included file does not contain UTF-8 text"))?;
Ok(ScopedJson::Derived(Value::String(url)))
} else { } else {
Err(RenderError::new("missing path string")) Err(RenderError::new("missing path string"))
} }

View file

@ -21,6 +21,7 @@ use crate::dirs::Dirs;
use crate::state::FileId; use crate::state::FileId;
use crate::state::Treehouse; use crate::state::Treehouse;
use crate::vfs; use crate::vfs;
use crate::vfs::ImageSize;
use super::highlight::highlight; use super::highlight::highlight;
@ -584,7 +585,9 @@ impl<'a> Writer<'a> {
write_attr(&url, out); write_attr(&url, out);
out.push('"'); out.push('"');
if let Some(image_size) = self.renderer.dirs.emoji.image_size(vpath) { if let Some(image_size) =
vfs::query::<ImageSize>(&self.renderer.dirs.emoji, vpath)
{
write!( write!(
out, out,
r#" width="{}" height="{}""#, r#" width="{}" height="{}""#,

View file

@ -12,7 +12,7 @@ use crate::{
parse::parse_tree_with_diagnostics, parse::parse_tree_with_diagnostics,
state::{report_diagnostics, Source, Treehouse}, state::{report_diagnostics, Source, Treehouse},
tree::SemaRoots, tree::SemaRoots,
vfs::{self, Cd, VPath, VPathBuf}, vfs::{self, Cd, Content, VPath, VPathBuf},
}; };
pub struct Sources { pub struct Sources {
@ -27,10 +27,8 @@ impl Sources {
let config = { let config = {
let _span = info_span!("load_config").entered(); let _span = info_span!("load_config").entered();
let mut config: Config = toml_edit::de::from_str( let mut config: Config = toml_edit::de::from_str(
&dirs &vfs::query::<Content>(&dirs.root, VPath::new_const("treehouse.toml"))
.root .map(Content::string)
.content(VPath::new("treehouse.toml"))
.map(String::from_utf8)
.ok_or_else(|| anyhow!("config file does not exist"))??, .ok_or_else(|| anyhow!("config file does not exist"))??,
) )
.context("failed to deserialize config")?; .context("failed to deserialize config")?;
@ -88,9 +86,8 @@ fn load_trees(config: &Config, dirs: &Dirs) -> anyhow::Result<Treehouse> {
.into_par_iter() .into_par_iter()
.zip(&file_ids) .zip(&file_ids)
.flat_map(|(path, &file_id)| { .flat_map(|(path, &file_id)| {
dirs.content vfs::query::<Content>(&dirs.content, &path)
.content(&path) .and_then(|c| c.string().ok())
.and_then(|b| String::from_utf8(b).ok())
.map(|input| { .map(|input| {
let parse_result = parse_tree_with_diagnostics(file_id, &input); let parse_result = parse_tree_with_diagnostics(file_id, &input);
(path, file_id, input, parse_result) (path, file_id, input, parse_result)

View file

@ -12,7 +12,7 @@ use crate::{
dirs::Dirs, dirs::Dirs,
html::EscapeHtml, html::EscapeHtml,
state::Treehouse, state::Treehouse,
vfs::{Dir, VPath}, vfs::{self, Content, VPath},
}; };
struct Lexer<'a> { struct Lexer<'a> {
@ -206,8 +206,8 @@ impl Renderer<'_> {
"pic" => Ok(config.pic_url(&*dirs.pic, arguments)), "pic" => Ok(config.pic_url(&*dirs.pic, arguments)),
"include_static" => VPath::try_new(arguments) "include_static" => VPath::try_new(arguments)
.ok() .ok()
.and_then(|vpath| dirs.static_.content(vpath)) .and_then(|vpath| vfs::query::<Content>(&dirs.static_, vpath))
.and_then(|content| String::from_utf8(content).ok()) .and_then(|c| c.string().ok())
.ok_or(InvalidTemplate), .ok_or(InvalidTemplate),
_ => Err(InvalidTemplate), _ => Err(InvalidTemplate),
} }

View file

@ -24,7 +24,6 @@
//! //!
//! In-memory directories can be composed using the following primitives: //! In-memory directories can be composed using the following primitives:
//! //!
//! - [`EmptyEntry`] - has no metadata whatsoever.
//! - [`BufferedFile`] - root path content is the provided byte vector. //! - [`BufferedFile`] - root path content is the provided byte vector.
//! - [`MemDir`] - a [`Dir`] containing a single level of other [`Dir`]s inside. //! - [`MemDir`] - a [`Dir`] containing a single level of other [`Dir`]s inside.
//! //!
@ -44,8 +43,10 @@
//! [`VPath`] also has an owned version, [`VPathBuf`]. //! [`VPath`] also has an owned version, [`VPathBuf`].
use std::{ use std::{
any::TypeId,
fmt::{self, Debug}, fmt::{self, Debug},
ops::{ControlFlow, Deref}, ops::{ControlFlow, Deref},
string::FromUtf8Error,
sync::Arc, sync::Arc,
}; };
@ -55,7 +56,6 @@ mod cd;
mod content_cache; mod content_cache;
mod content_version_cache; mod content_version_cache;
mod edit; mod edit;
mod empty;
mod file; mod file;
mod html_canonicalize; mod html_canonicalize;
mod image_size_cache; mod image_size_cache;
@ -69,7 +69,6 @@ pub use cd::*;
pub use content_cache::*; pub use content_cache::*;
pub use content_version_cache::*; pub use content_version_cache::*;
pub use edit::*; pub use edit::*;
pub use empty::*;
pub use file::*; pub use file::*;
pub use html_canonicalize::*; pub use html_canonicalize::*;
pub use image_size_cache::*; pub use image_size_cache::*;
@ -78,50 +77,94 @@ pub use overlay::*;
pub use path::*; pub use path::*;
pub use physical::*; pub use physical::*;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct DirEntry {
pub path: VPathBuf,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct ImageSize {
pub width: u32,
pub height: u32,
}
pub trait Dir: Debug { pub trait Dir: Debug {
/// List all entries under the provided path. fn query(&self, path: &VPath, query: &mut Query);
fn dir(&self, path: &VPath) -> Vec<DirEntry>; }
/// Return the byte content of the entry at the given path. pub trait Fork {}
fn content(&self, path: &VPath) -> Option<Vec<u8>>;
/// Get a string signifying the current version of the provided path's content. pub fn query<'a, T>(dir: &'a (impl Dir + ?Sized), path: &VPath) -> Option<T>
/// If the content changes, the version must also change. where
/// T: 'static + Fork,
/// Returns None if there is no content or no version string is available. {
fn content_version(&self, path: &VPath) -> Option<String>; let mut slot = TaggedOption::<'a, tags::Value<T>>(None);
dir.query(path, Query::new(&mut slot));
slot.0
}
/// Returns the size of the image at the given path, or `None` if the entry is not an image #[repr(transparent)]
/// (or its size cannot be known.) pub struct Query<'a> {
fn image_size(&self, _path: &VPath) -> Option<ImageSize> { erased: dyn Erased<'a> + 'a,
None }
impl<'a> Query<'a> {
fn new<'b>(erased: &'b mut (dyn Erased<'a> + 'a)) -> &'b mut Query<'a> {
unsafe { &mut *(erased as *mut dyn Erased<'a> as *mut Query<'a>) }
} }
/// Returns a path relative to `config.site` indicating where the file will be available pub fn provide<T>(&mut self, f: impl FnOnce() -> T)
/// once served. where
/// T: 'static + Fork,
/// May return `None` if the file is not served. {
fn anchor(&self, _path: &VPath) -> Option<VPathBuf> { if let Some(result @ TaggedOption(None)) = self.erased.downcast_mut::<tags::Value<T>>() {
None result.0 = Some(f());
}
} }
/// If a file can be written persistently, returns an [`EditPath`] representing the file in pub fn try_provide<T>(&mut self, f: impl FnOnce() -> Option<T>)
/// persistent storage. where
/// T: 'static + Fork,
/// An edit path can then be made into an [`Edit`]. {
fn edit_path(&self, _path: &VPath) -> Option<EditPath> { if let Some(result @ TaggedOption(None)) = self.erased.downcast_mut::<tags::Value<T>>() {
None result.0 = f();
}
}
}
mod tags {
use std::marker::PhantomData;
pub trait Type<'a>: Sized + 'static {
type Reified: 'a;
}
pub struct Value<T>(PhantomData<T>)
where
T: 'static;
impl<T> Type<'_> for Value<T>
where
T: 'static,
{
type Reified = T;
}
}
#[repr(transparent)]
struct TaggedOption<'a, I: tags::Type<'a>>(Option<I::Reified>);
#[expect(clippy::missing_safety_doc)]
unsafe trait Erased<'a>: 'a {
fn tag_id(&self) -> TypeId;
}
unsafe impl<'a, I: tags::Type<'a>> Erased<'a> for TaggedOption<'a, I> {
fn tag_id(&self) -> TypeId {
TypeId::of::<I>()
}
}
impl<'a> dyn Erased<'a> + 'a {
fn downcast_mut<I>(&mut self) -> Option<&mut TaggedOption<'a, I>>
where
I: tags::Type<'a>,
{
if self.tag_id() == TypeId::of::<I>() {
// SAFETY: Just checked whether we're pointing to an I.
Some(unsafe { &mut *(self as *mut Self).cast::<TaggedOption<'a, I>>() })
} else {
None
}
} }
} }
@ -129,28 +172,8 @@ impl<T> Dir for &T
where where
T: Dir, T: Dir,
{ {
fn dir(&self, path: &VPath) -> Vec<DirEntry> { fn query(&self, path: &VPath, query: &mut Query) {
(**self).dir(path) (**self).query(path, query)
}
fn content(&self, path: &VPath) -> Option<Vec<u8>> {
(**self).content(path)
}
fn content_version(&self, path: &VPath) -> Option<String> {
(**self).content_version(path)
}
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
(**self).image_size(path)
}
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
(**self).anchor(path)
}
fn edit_path(&self, path: &VPath) -> Option<EditPath> {
(**self).edit_path(path)
} }
} }
@ -160,28 +183,8 @@ pub struct DynDir {
} }
impl Dir for DynDir { impl Dir for DynDir {
fn dir(&self, path: &VPath) -> Vec<DirEntry> { fn query(&self, path: &VPath, query: &mut Query) {
self.arc.dir(path) self.arc.query(path, query);
}
fn content(&self, path: &VPath) -> Option<Vec<u8>> {
self.arc.content(path)
}
fn content_version(&self, path: &VPath) -> Option<String> {
self.arc.content_version(path)
}
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
self.arc.image_size(path)
}
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
self.arc.anchor(path)
}
fn edit_path(&self, path: &VPath) -> Option<EditPath> {
self.arc.edit_path(path)
} }
} }
@ -229,21 +232,75 @@ where
} }
} }
/// List of child entries under a directory.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Entries(pub Vec<VPathBuf>);
/// Byte content in an entry.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Content {
bytes: Vec<u8>,
}
/// Abstract version of an entry.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct ContentVersion {
pub string: String,
}
/// Path relative to `config.site` indicating where the file will be available once served.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Anchor {
pub path: VPathBuf,
}
/// Size of image entries.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct ImageSize {
pub width: u32,
pub height: u32,
}
impl Content {
pub fn new(bytes: Vec<u8>) -> Self {
Self { bytes }
}
pub fn bytes(self) -> Vec<u8> {
self.bytes
}
pub fn string(self) -> Result<String, FromUtf8Error> {
String::from_utf8(self.bytes())
}
}
impl Fork for Entries {}
impl Fork for Content {}
impl Fork for ContentVersion {}
impl Fork for Anchor {}
impl Fork for ImageSize {}
impl Fork for EditPath {}
pub fn entries(dir: &dyn Dir, path: &VPath) -> Vec<VPathBuf> {
query::<Entries>(dir, path).map(|e| e.0).unwrap_or_default()
}
pub fn walk_dir_rec(dir: &dyn Dir, path: &VPath, f: &mut dyn FnMut(&VPath) -> ControlFlow<(), ()>) { pub fn walk_dir_rec(dir: &dyn Dir, path: &VPath, f: &mut dyn FnMut(&VPath) -> ControlFlow<(), ()>) {
for entry in dir.dir(path) { for entry in entries(dir, path) {
match f(&entry.path) { match f(&entry) {
ControlFlow::Continue(_) => (), ControlFlow::Continue(_) => (),
ControlFlow::Break(_) => return, ControlFlow::Break(_) => return,
} }
walk_dir_rec(dir, &entry.path, f); walk_dir_rec(dir, &entry, f);
} }
} }
pub fn url(site: &str, dir: &dyn Dir, path: &VPath) -> Option<String> { pub fn url(site: &str, dir: &dyn Dir, path: &VPath) -> Option<String> {
let anchor = dir.anchor(path)?; let anchor = query::<Anchor>(dir, path)?;
if let Some(version) = dir.content_version(path) { if let Some(version) = query::<ContentVersion>(dir, path) {
Some(format!("{}/{anchor}?v={version}", site)) Some(format!("{}/{}?v={}", site, anchor.path, version.string))
} else { } else {
Some(format!("{}/{anchor}", site)) Some(format!("{}/{}", site, anchor.path))
} }
} }

View file

@ -1,6 +1,6 @@
use std::fmt; use std::fmt;
use super::{Dir, DirEntry, EditPath, ImageSize, VPath, VPathBuf}; use super::{Anchor, Dir, Query, VPath, VPathBuf};
pub struct Anchored<T> { pub struct Anchored<T> {
inner: T, inner: T,
@ -17,28 +17,12 @@ impl<T> Dir for Anchored<T>
where where
T: Dir, T: Dir,
{ {
fn dir(&self, path: &VPath) -> Vec<DirEntry> { fn query(&self, path: &VPath, query: &mut Query) {
self.inner.dir(path) query.provide(|| Anchor {
} path: self.at.join(path),
});
fn content(&self, path: &VPath) -> Option<Vec<u8>> { self.inner.query(path, query);
self.inner.content(path)
}
fn content_version(&self, path: &VPath) -> Option<String> {
self.inner.content_version(path)
}
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
self.inner.image_size(path)
}
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
Some(self.at.join(path))
}
fn edit_path(&self, path: &VPath) -> Option<EditPath> {
self.inner.edit_path(path)
} }
} }

View file

@ -1,4 +1,4 @@
use super::{Dir, DynDir, VPath}; use super::{query, Content, DynDir, VPath};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct AsyncDir { pub struct AsyncDir {
@ -10,13 +10,13 @@ impl AsyncDir {
Self { inner } Self { inner }
} }
pub async fn content(&self, path: &VPath) -> Option<Vec<u8>> { pub async fn content(&self, path: &VPath) -> Option<Content> {
let this = self.clone(); let this = self.clone();
let path = path.to_owned(); let path = path.to_owned();
// NOTE: Performance impact of spawning a blocking task may be a bit high in case // NOTE: Performance impact of spawning a blocking task may be a bit high in case
// we add caching. // we add caching.
// Measure throughput here. // Measure throughput here.
tokio::task::spawn_blocking(move || this.inner.content(&path)) tokio::task::spawn_blocking(move || query::<Content>(&this.inner, &path))
.await .await
.unwrap() .unwrap()
} }

View file

@ -1,6 +1,6 @@
use std::fmt; use std::fmt;
use super::{Dir, DirEntry, EditPath, ImageSize, VPath, VPathBuf}; use super::{entries, Dir, Entries, Query, VPath, VPathBuf};
pub struct Cd<T> { pub struct Cd<T> {
parent: T, parent: T,
@ -13,42 +13,34 @@ impl<T> Cd<T> {
} }
} }
impl<T> Cd<T>
where
T: Dir,
{
fn dir(&self, path: &VPath) -> Vec<VPathBuf> {
entries(&self.parent, &self.path.join(path))
.into_iter()
.map(|entry| {
entry
.strip_prefix(&self.path)
.expect("all entries must be anchored within `self.path`")
.to_owned()
})
.collect()
}
}
impl<T> Dir for Cd<T> impl<T> Dir for Cd<T>
where where
T: Dir, T: Dir,
{ {
fn dir(&self, path: &VPath) -> Vec<DirEntry> { fn query(&self, path: &VPath, query: &mut Query) {
self.parent // The only query that meaningfully needs to return something else is `dir`, which must
.dir(&self.path.join(path)) // be modified to strip prefixes off of the parent's returned paths.
.into_iter() query.provide(|| Entries(self.dir(path)));
.map(|entry| DirEntry {
path: entry
.path
.strip_prefix(&self.path)
.expect("all entries must be anchored within `self.path`")
.to_owned(),
})
.collect()
}
fn content_version(&self, path: &VPath) -> Option<String> { // Other queries can run unmodified, only passing them the right path.
self.parent.content_version(&self.path.join(path)) self.parent.query(&self.path.join(path), query);
}
fn content(&self, path: &VPath) -> Option<Vec<u8>> {
self.parent.content(&self.path.join(path))
}
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
self.parent.image_size(&self.path.join(path))
}
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
self.parent.anchor(&self.path.join(path))
}
fn edit_path(&self, path: &VPath) -> Option<EditPath> {
self.parent.edit_path(&self.path.join(path))
} }
} }

View file

@ -7,11 +7,11 @@ use dashmap::DashMap;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use tracing::{info_span, instrument}; use tracing::{info_span, instrument};
use super::{walk_dir_rec, Dir, DirEntry, EditPath, ImageSize, VPath, VPathBuf}; use super::{query, walk_dir_rec, Content, Dir, Query, VPath, VPathBuf};
pub struct ContentCache<T> { pub struct ContentCache<T> {
inner: T, inner: T,
cache: DashMap<VPathBuf, Vec<u8>>, cache: DashMap<VPathBuf, Content>,
} }
impl<T> ContentCache<T> { impl<T> ContentCache<T> {
@ -39,41 +39,32 @@ where
} }
} }
impl<T> Dir for ContentCache<T> impl<T> ContentCache<T>
where where
T: Dir, T: Dir,
{ {
fn dir(&self, path: &VPath) -> Vec<DirEntry> {
self.inner.dir(path)
}
#[instrument(name = "ContentCache::content", skip(self))] #[instrument(name = "ContentCache::content", skip(self))]
fn content(&self, path: &VPath) -> Option<Vec<u8>> { fn content(&self, path: &VPath) -> Option<Content> {
self.cache.get(path).map(|x| x.clone()).or_else(|| { self.cache.get(path).map(|x| x.clone()).or_else(|| {
let _span = info_span!("cache_miss").entered(); let _span = info_span!("cache_miss").entered();
let content = self.inner.content(path); let content = query::<Content>(&self.inner, path);
if let Some(content) = &content { if let Some(content) = &content {
self.cache.insert(path.to_owned(), content.clone()); self.cache.insert(path.to_owned(), content.clone());
} }
content content
}) })
} }
}
fn content_version(&self, path: &VPath) -> Option<String> { impl<T> Dir for ContentCache<T>
self.inner.content_version(path) where
} T: Dir,
{
fn query(&self, path: &VPath, query: &mut Query) {
query.try_provide(|| self.content(path));
fn image_size(&self, path: &VPath) -> Option<ImageSize> { self.inner.query(path, query);
self.inner.image_size(path)
}
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
self.inner.anchor(path)
}
fn edit_path(&self, path: &VPath) -> Option<EditPath> {
self.inner.edit_path(path)
} }
} }

View file

@ -3,11 +3,11 @@ use std::fmt::{self, Debug};
use dashmap::DashMap; use dashmap::DashMap;
use tracing::{info_span, instrument}; use tracing::{info_span, instrument};
use super::{Dir, DirEntry, EditPath, ImageSize, VPath, VPathBuf}; use super::{query, Content, ContentVersion, Dir, Query, VPath, VPathBuf};
pub struct Blake3ContentVersionCache<T> { pub struct Blake3ContentVersionCache<T> {
inner: T, inner: T,
cache: DashMap<VPathBuf, String>, cache: DashMap<VPathBuf, ContentVersion>,
} }
impl<T> Blake3ContentVersionCache<T> { impl<T> Blake3ContentVersionCache<T> {
@ -19,26 +19,20 @@ impl<T> Blake3ContentVersionCache<T> {
} }
} }
impl<T> Dir for Blake3ContentVersionCache<T> impl<T> Blake3ContentVersionCache<T>
where where
T: Dir, T: Dir,
{ {
fn dir(&self, path: &VPath) -> Vec<DirEntry> {
self.inner.dir(path)
}
fn content(&self, path: &VPath) -> Option<Vec<u8>> {
self.inner.content(path)
}
#[instrument(name = "Blake3ContentVersionCache::content_version", skip(self))] #[instrument(name = "Blake3ContentVersionCache::content_version", skip(self))]
fn content_version(&self, path: &VPath) -> Option<String> { fn content_version(&self, path: &VPath) -> Option<ContentVersion> {
self.cache.get(path).map(|x| x.clone()).or_else(|| { self.cache.get(path).map(|x| x.clone()).or_else(|| {
let _span = info_span!("cache_miss").entered(); let _span = info_span!("cache_miss").entered();
let version = self.inner.content(path).map(|content| { let version = query::<Content>(&self.inner, path).map(|content| {
let hash = blake3::hash(&content).to_hex(); let hash = blake3::hash(&content.bytes()).to_hex();
format!("b3-{}", &hash[0..8]) ContentVersion {
string: format!("b3-{}", &hash[0..8]),
}
}); });
if let Some(version) = &version { if let Some(version) = &version {
self.cache.insert(path.to_owned(), version.clone()); self.cache.insert(path.to_owned(), version.clone());
@ -46,17 +40,16 @@ where
version version
}) })
} }
}
fn image_size(&self, path: &VPath) -> Option<ImageSize> { impl<T> Dir for Blake3ContentVersionCache<T>
self.inner.image_size(path) where
} T: Dir,
{
fn query(&self, path: &VPath, query: &mut Query) {
query.try_provide(|| self.content_version(path));
fn anchor(&self, path: &VPath) -> Option<VPathBuf> { self.inner.query(path, query);
self.inner.anchor(path)
}
fn edit_path(&self, path: &VPath) -> Option<EditPath> {
self.inner.edit_path(path)
} }
} }

View file

@ -1,18 +0,0 @@
use super::{Dir, DirEntry, VPath};
#[derive(Debug)]
pub struct EmptyEntry;
impl Dir for EmptyEntry {
fn dir(&self, _path: &VPath) -> Vec<DirEntry> {
vec![]
}
fn content_version(&self, _path: &VPath) -> Option<String> {
None
}
fn content(&self, _path: &VPath) -> Option<Vec<u8>> {
None
}
}

View file

@ -1,6 +1,6 @@
use std::fmt; use std::fmt;
use super::{DirEntry, Dir, VPath}; use super::{Content, Dir, Query, VPath};
pub struct BufferedFile { pub struct BufferedFile {
pub content: Vec<u8>, pub content: Vec<u8>,
@ -13,20 +13,9 @@ impl BufferedFile {
} }
impl Dir for BufferedFile { impl Dir for BufferedFile {
fn dir(&self, _path: &VPath) -> Vec<DirEntry> { fn query(&self, path: &VPath, query: &mut Query) {
vec![]
}
fn content_version(&self, _path: &VPath) -> Option<String> {
// TODO: StaticFile should _probably_ calculate a content_version.
None
}
fn content(&self, path: &VPath) -> Option<Vec<u8>> {
if path == VPath::ROOT { if path == VPath::ROOT {
Some(self.content.clone()) query.provide(|| Content::new(self.content.clone()));
} else {
None
} }
} }
} }

View file

@ -1,6 +1,6 @@
use core::fmt; use core::fmt;
use super::{Dir, DirEntry, EditPath, ImageSize, VPath, VPathBuf}; use super::{Dir, Query, VPath};
pub struct HtmlCanonicalize<T> { pub struct HtmlCanonicalize<T> {
inner: T, inner: T,
@ -16,33 +16,13 @@ impl<T> Dir for HtmlCanonicalize<T>
where where
T: Dir, T: Dir,
{ {
fn dir(&self, path: &VPath) -> Vec<DirEntry> { fn query(&self, path: &VPath, query: &mut Query) {
self.inner.dir(path)
}
fn content(&self, path: &VPath) -> Option<Vec<u8>> {
let mut path = path.to_owned(); let mut path = path.to_owned();
if path.extension() == Some("html") { if path.extension() == Some("html") {
path.set_extension(""); path.set_extension("");
} }
self.inner.content(&path) self.inner.query(&path, query);
}
fn content_version(&self, path: &VPath) -> Option<String> {
self.inner.content_version(path)
}
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
self.inner.image_size(path)
}
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
self.inner.anchor(path)
}
fn edit_path(&self, path: &VPath) -> Option<EditPath> {
self.inner.edit_path(path)
} }
} }

View file

@ -6,7 +6,7 @@ use tracing::{info_span, instrument, warn};
use crate::config; use crate::config;
use super::{Dir, DirEntry, EditPath, ImageSize, VPath, VPathBuf}; use super::{query, Content, Dir, ImageSize, Query, VPath, VPathBuf};
pub struct ImageSizeCache<T> { pub struct ImageSizeCache<T> {
inner: T, inner: T,
@ -28,8 +28,8 @@ where
{ {
fn compute_image_size(&self, path: &VPath) -> anyhow::Result<Option<ImageSize>> { fn compute_image_size(&self, path: &VPath) -> anyhow::Result<Option<ImageSize>> {
if path.extension().is_some_and(config::is_image_file) { if path.extension().is_some_and(config::is_image_file) {
if let Some(content) = self.content(path) { if let Some(content) = query::<Content>(&self.inner, path) {
let reader = image::ImageReader::new(Cursor::new(content)) let reader = image::ImageReader::new(Cursor::new(content.bytes()))
.with_guessed_format() .with_guessed_format()
.context("cannot guess image format")?; .context("cannot guess image format")?;
let (width, height) = reader.into_dimensions()?; let (width, height) = reader.into_dimensions()?;
@ -39,23 +39,6 @@ where
Ok(None) Ok(None)
} }
}
impl<T> Dir for ImageSizeCache<T>
where
T: Dir,
{
fn dir(&self, path: &VPath) -> Vec<DirEntry> {
self.inner.dir(path)
}
fn content(&self, path: &VPath) -> Option<Vec<u8>> {
self.inner.content(path)
}
fn content_version(&self, path: &VPath) -> Option<String> {
self.inner.content_version(path)
}
#[instrument("ImageSizeCache::image_size", skip(self))] #[instrument("ImageSizeCache::image_size", skip(self))]
fn image_size(&self, path: &VPath) -> Option<ImageSize> { fn image_size(&self, path: &VPath) -> Option<ImageSize> {
@ -73,13 +56,16 @@ where
image_size image_size
}) })
} }
}
fn anchor(&self, path: &VPath) -> Option<VPathBuf> { impl<T> Dir for ImageSizeCache<T>
self.inner.anchor(path) where
} T: Dir,
{
fn query(&self, path: &VPath, query: &mut Query) {
query.try_provide(|| self.image_size(path));
fn edit_path(&self, path: &VPath) -> Option<EditPath> { self.inner.query(path, query);
self.inner.edit_path(path)
} }
} }

View file

@ -1,6 +1,6 @@
use std::{collections::HashMap, fmt}; use std::{collections::HashMap, fmt};
use super::{Dir, DirEntry, DynDir, EditPath, ImageSize, VPath, VPathBuf}; use super::{entries, Dir, DynDir, Entries, Query, VPath, VPathBuf};
pub struct MemDir { pub struct MemDir {
mount_points: HashMap<String, DynDir>, mount_points: HashMap<String, DynDir>,
@ -55,6 +55,23 @@ impl MemDir {
Resolved::None Resolved::None
} }
fn dir(&self, path: &VPath) -> Vec<VPathBuf> {
match self.resolve(path) {
Resolved::Root => self.mount_points.keys().map(VPathBuf::new).collect(),
Resolved::MountPoint {
fs,
fs_path,
subpath,
} => entries(fs, subpath)
.into_iter()
.map(|path| fs_path.join(&path))
.collect(),
Resolved::None => vec![],
}
}
} }
impl Default for MemDir { impl Default for MemDir {
@ -64,82 +81,16 @@ impl Default for MemDir {
} }
impl Dir for MemDir { impl Dir for MemDir {
fn dir(&self, path: &VPath) -> Vec<DirEntry> { fn query(&self, path: &VPath, query: &mut Query) {
match self.resolve(path) { query.provide(|| Entries(self.dir(path)));
Resolved::Root => self
.mount_points
.keys()
.map(|name| DirEntry {
path: VPathBuf::new(name),
})
.collect(),
Resolved::MountPoint {
fs,
fs_path,
subpath,
} => fs
.dir(subpath)
.into_iter()
.map(|entry| DirEntry {
path: fs_path.join(&entry.path),
})
.collect(),
Resolved::None => vec![],
}
}
fn content_version(&self, path: &VPath) -> Option<String> {
match self.resolve(path) { match self.resolve(path) {
Resolved::Root | Resolved::None => (),
Resolved::MountPoint { Resolved::MountPoint {
fs, fs,
fs_path: _, fs_path: _,
subpath, subpath,
} => fs.content_version(subpath), } => fs.query(subpath, query),
Resolved::Root | Resolved::None => None,
}
}
fn content(&self, path: &VPath) -> Option<Vec<u8>> {
match self.resolve(path) {
Resolved::MountPoint {
fs,
fs_path: _,
subpath,
} => fs.content(subpath),
Resolved::Root | Resolved::None => None,
}
}
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
match self.resolve(path) {
Resolved::MountPoint {
fs,
fs_path: _,
subpath,
} => fs.image_size(subpath),
Resolved::Root | Resolved::None => None,
}
}
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
match self.resolve(path) {
Resolved::MountPoint {
fs,
fs_path: _,
subpath,
} => fs.anchor(subpath),
Resolved::Root | Resolved::None => None,
}
}
fn edit_path(&self, path: &VPath) -> Option<EditPath> {
match self.resolve(path) {
Resolved::MountPoint {
fs,
fs_path: _,
subpath,
} => fs.edit_path(subpath),
Resolved::Root | Resolved::None => None,
} }
} }
} }

View file

@ -2,7 +2,7 @@ use std::fmt;
use tracing::instrument; use tracing::instrument;
use super::{Dir, DirEntry, DynDir, EditPath, ImageSize, VPath, VPathBuf}; use super::{entries, Dir, DynDir, Entries, Query, VPath, VPathBuf};
pub struct Overlay { pub struct Overlay {
base: DynDir, base: DynDir,
@ -13,44 +13,23 @@ impl Overlay {
pub fn new(base: DynDir, overlay: DynDir) -> Self { pub fn new(base: DynDir, overlay: DynDir) -> Self {
Self { base, overlay } Self { base, overlay }
} }
}
impl Dir for Overlay {
#[instrument("Overlay::dir", skip(self))] #[instrument("Overlay::dir", skip(self))]
fn dir(&self, path: &VPath) -> Vec<DirEntry> { fn dir(&self, path: &VPath) -> Vec<VPathBuf> {
let mut dir = self.base.dir(path); let mut dir = entries(&self.base, path);
dir.append(&mut self.overlay.dir(path)); dir.append(&mut entries(&self.overlay, path));
dir.sort(); dir.sort();
dir.dedup(); dir.dedup();
dir dir
} }
}
fn content(&self, path: &VPath) -> Option<Vec<u8>> { impl Dir for Overlay {
self.overlay fn query(&self, path: &VPath, query: &mut Query) {
.content(path) query.provide(|| Entries(self.dir(path)));
.or_else(|| self.base.content(path))
}
fn content_version(&self, path: &VPath) -> Option<String> { self.overlay.query(path, query);
self.overlay self.base.query(path, query);
.content_version(path)
.or_else(|| self.base.content_version(path))
}
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
self.overlay
.image_size(path)
.or_else(|| self.base.image_size(path))
}
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
self.overlay.anchor(path).or_else(|| self.base.anchor(path))
}
fn edit_path(&self, path: &VPath) -> Option<EditPath> {
self.overlay
.edit_path(path)
.or_else(|| self.base.edit_path(path))
} }
} }

View file

@ -2,7 +2,7 @@ use std::path::{Path, PathBuf};
use tracing::{error, instrument}; use tracing::{error, instrument};
use super::{Dir, DirEntry, EditPath, VPath, VPathBuf}; use super::{Content, Dir, EditPath, Entries, Query, VPath, VPathBuf};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct PhysicalDir { pub struct PhysicalDir {
@ -13,11 +13,8 @@ impl PhysicalDir {
pub fn new(root: PathBuf) -> Self { pub fn new(root: PathBuf) -> Self {
Self { root } Self { root }
} }
}
impl Dir for PhysicalDir { fn entries(&self, vpath: &VPath) -> Vec<VPathBuf> {
#[instrument("PhysicalDir::dir", skip(self))]
fn dir(&self, vpath: &VPath) -> Vec<DirEntry> {
let physical = self.root.join(physical_path(vpath)); let physical = self.root.join(physical_path(vpath));
if !physical.is_dir() { if !physical.is_dir() {
return vec![]; return vec![];
@ -47,7 +44,7 @@ impl Dir for PhysicalDir {
error!("{self:?} error with vpath for {path_str:?}: {err:?}"); error!("{self:?} error with vpath for {path_str:?}: {err:?}");
}) })
.ok()?; .ok()?;
Some(DirEntry { path: vpath_buf }) Some(vpath_buf)
}) })
}) })
.collect(), .collect(),
@ -60,21 +57,26 @@ impl Dir for PhysicalDir {
} }
} }
fn content_version(&self, _path: &VPath) -> Option<String> {
None
}
#[instrument("PhysicalDir::content", skip(self))] #[instrument("PhysicalDir::content", skip(self))]
fn content(&self, path: &VPath) -> Option<Vec<u8>> { fn content(&self, path: &VPath) -> Option<Content> {
std::fs::read(self.root.join(physical_path(path))) std::fs::read(self.root.join(physical_path(path)))
.inspect_err(|err| error!("{self:?} cannot read file at vpath {path:?}: {err:?}",)) .inspect_err(|err| error!("{self:?} cannot read file at vpath {path:?}: {err:?}",))
.ok() .ok()
.map(Content::new)
} }
fn edit_path(&self, path: &VPath) -> Option<EditPath> { fn edit_path(&self, path: &VPath) -> EditPath {
Some(EditPath { EditPath {
path: self.root.join(physical_path(path)), 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));
} }
} }

View file

@ -1,5 +1,4 @@
mod cd; mod cd;
mod empty;
mod file; mod file;
mod mount_points; mod mount_points;
mod physical; mod physical;

View file

@ -1,4 +1,6 @@
use treehouse::vfs::{BufferedFile, Cd, Dir, DirEntry, MemDir, ToDynDir, VPath, VPathBuf}; use treehouse::vfs::{
entries, query, BufferedFile, Cd, Content, MemDir, ToDynDir, VPath, VPathBuf,
};
const HEWWO: &[u8] = b"hewwo :3"; const HEWWO: &[u8] = b"hewwo :3";
const FWOOFEE: &[u8] = b"fwoofee -w-"; const FWOOFEE: &[u8] = b"fwoofee -w-";
@ -27,20 +29,14 @@ fn dir1() {
let outer = vfs(); let outer = vfs();
let inner = Cd::new(outer, VPathBuf::new("inner")); let inner = Cd::new(outer, VPathBuf::new("inner"));
let mut dir = inner.dir(VPath::ROOT); let mut dir = entries(&inner, VPath::ROOT);
dir.sort(); dir.sort();
assert_eq!( assert_eq!(
dir, dir,
vec![ vec![
DirEntry { VPathBuf::new("file1.txt"),
path: VPathBuf::new("file1.txt"), VPathBuf::new("file2.txt"),
}, VPathBuf::new("innermost"),
DirEntry {
path: VPathBuf::new("file2.txt"),
},
DirEntry {
path: VPathBuf::new("innermost"),
}
] ]
); );
} }
@ -50,25 +46,9 @@ fn dir2() {
let outer = vfs(); let outer = vfs();
let innermost = Cd::new(&outer, VPathBuf::new("inner/innermost")); let innermost = Cd::new(&outer, VPathBuf::new("inner/innermost"));
let mut dir = innermost.dir(VPath::ROOT); let mut dir = entries(&innermost, VPath::ROOT);
dir.sort(); dir.sort();
assert_eq!( assert_eq!(dir, vec![VPathBuf::new("file3.txt")]);
dir,
vec![DirEntry {
path: VPathBuf::new("file3.txt"),
},]
);
}
#[test]
fn content_version() {
let outer = vfs();
let inner = Cd::new(&outer, VPathBuf::new("inner"));
assert_eq!(
inner.content_version(VPath::new("test1.txt")),
outer.content_version(VPath::new("inner/test1.txt"))
);
} }
#[test] #[test]
@ -77,7 +57,7 @@ fn content() {
let inner = Cd::new(&outer, VPathBuf::new("inner")); let inner = Cd::new(&outer, VPathBuf::new("inner"));
assert_eq!( assert_eq!(
inner.content(VPath::new("test1.txt")), query::<Content>(&inner, VPath::new("test1.txt")).map(Content::bytes),
outer.content(VPath::new("inner/test1.txt")) query::<Content>(&outer, VPath::new("inner/test1.txt")).map(Content::bytes)
); );
} }

View file

@ -1,16 +0,0 @@
use treehouse::vfs::{Dir, EmptyEntry, VPath};
#[test]
fn dir() {
assert!(EmptyEntry.dir(VPath::ROOT).is_empty());
}
#[test]
fn content_version() {
assert!(EmptyEntry.content_version(VPath::ROOT).is_none());
}
#[test]
fn content() {
assert!(EmptyEntry.content(VPath::ROOT).is_none());
}

View file

@ -1,4 +1,4 @@
use treehouse::vfs::{BufferedFile, Dir, VPath}; use treehouse::vfs::{entries, query, BufferedFile, Content, VPath};
fn vfs() -> BufferedFile { fn vfs() -> BufferedFile {
BufferedFile::new(b"hewwo :3".to_vec()) BufferedFile::new(b"hewwo :3".to_vec())
@ -7,23 +7,16 @@ fn vfs() -> BufferedFile {
#[test] #[test]
fn dir() { fn dir() {
let vfs = vfs(); let vfs = vfs();
assert!(vfs.dir(VPath::ROOT).is_empty()); assert!(entries(&vfs, VPath::ROOT).is_empty());
}
#[test]
fn content_version() {
let vfs = vfs();
assert!(
vfs.content_version(VPath::ROOT).is_none(),
"content_version is not implemented for BufferedFile for now"
);
} }
#[test] #[test]
fn content() { fn content() {
let vfs = vfs(); let vfs = vfs();
assert_eq!( assert_eq!(
vfs.content(VPath::ROOT).as_deref(), query::<Content>(&vfs, VPath::ROOT)
.map(|c| c.bytes())
.as_deref(),
Some(b"hewwo :3".as_slice()), Some(b"hewwo :3".as_slice()),
); );
} }

View file

@ -1,4 +1,6 @@
use treehouse::vfs::{BufferedFile, Dir, DirEntry, MemDir, ToDynDir, VPath, VPathBuf}; use treehouse::vfs::{
entries, query, BufferedFile, Content, Dir, MemDir, ToDynDir, VPath, VPathBuf,
};
const HEWWO: &[u8] = b"hewwo :3"; const HEWWO: &[u8] = b"hewwo :3";
const FWOOFEE: &[u8] = b"fwoofee -w-"; const FWOOFEE: &[u8] = b"fwoofee -w-";
@ -23,52 +25,22 @@ fn vfs() -> MemDir {
fn dir() { fn dir() {
let vfs = vfs(); let vfs = vfs();
let mut dir = vfs.dir(VPath::new("")); let mut dir = entries(&vfs, VPath::ROOT);
dir.sort(); dir.sort();
assert_eq!( assert_eq!(
dir, dir,
vec![ vec![
DirEntry { VPathBuf::new("file1.txt"),
path: VPathBuf::new("file1.txt"), VPathBuf::new("file2.txt"),
}, VPathBuf::new("inner"),
DirEntry {
path: VPathBuf::new("file2.txt"),
},
DirEntry {
path: VPathBuf::new("inner"),
}
] ]
); );
assert!(vfs.dir(VPath::new("file1.txt")).is_empty()); assert!(entries(&vfs, VPath::new("file1.txt")).is_empty());
assert!(vfs.dir(VPath::new("file2.txt")).is_empty()); assert!(entries(&vfs, VPath::new("file2.txt")).is_empty());
assert_eq!( assert_eq!(
vfs.dir(VPath::new("inner")), entries(&vfs, VPath::new("inner")),
vec![DirEntry { vec![VPathBuf::new("inner/file3.txt")]
path: VPathBuf::new("inner/file3.txt")
}]
);
}
#[test]
fn content_version() {
let vfs = vfs();
let file1 = BufferedFile::new(HEWWO.to_vec());
let file2 = BufferedFile::new(FWOOFEE.to_vec());
let file3 = BufferedFile::new(BOOP.to_vec());
assert_eq!(
vfs.content_version(VPath::new("file1.txt")),
file1.content_version(VPath::ROOT)
);
assert_eq!(
vfs.content_version(VPath::new("file2.txt")),
file2.content_version(VPath::ROOT)
);
assert_eq!(
vfs.content_version(VPath::new("inner/file3.txt")),
file3.content_version(VPath::ROOT)
); );
} }
@ -76,13 +48,22 @@ fn content_version() {
fn content() { fn content() {
let vfs = vfs(); let vfs = vfs();
assert_eq!(vfs.content(VPath::new("file1.txt")).as_deref(), Some(HEWWO));
assert_eq!( assert_eq!(
vfs.content(VPath::new("file2.txt")).as_deref(), query::<Content>(&vfs, VPath::new("file1.txt"))
.map(Content::bytes)
.as_deref(),
Some(HEWWO)
);
assert_eq!(
query::<Content>(&vfs, VPath::new("file2.txt"))
.map(Content::bytes)
.as_deref(),
Some(FWOOFEE) Some(FWOOFEE)
); );
assert_eq!( assert_eq!(
vfs.content(VPath::new("inner/file3.txt")).as_deref(), query::<Content>(&vfs, VPath::new("inner/file3.txt"))
.map(Content::bytes)
.as_deref(),
Some(BOOP) Some(BOOP)
); );
} }

View file

@ -1,6 +1,6 @@
use std::path::Path; use std::path::Path;
use treehouse::vfs::{DirEntry, PhysicalDir, Dir, VPath, VPathBuf}; use treehouse::vfs::{entries, query, Content, PhysicalDir, VPath, VPathBuf};
fn vfs() -> PhysicalDir { fn vfs() -> PhysicalDir {
let root = Path::new("tests/it/vfs_physical").to_path_buf(); let root = Path::new("tests/it/vfs_physical").to_path_buf();
@ -10,28 +10,13 @@ fn vfs() -> PhysicalDir {
#[test] #[test]
fn dir() { fn dir() {
let vfs = vfs(); let vfs = vfs();
let dir = vfs.dir(VPath::ROOT); let dir = entries(&vfs, VPath::ROOT);
assert_eq!( assert_eq!(&dir[..], &[VPathBuf::new("test.txt")]);
&dir[..],
&[DirEntry {
path: VPathBuf::new("test.txt"),
}]
);
}
#[test]
fn content_version() {
let vfs = vfs();
let content_version = vfs.content_version(VPath::new("test.txt"));
assert_eq!(
content_version, None,
"content_version remains unimplemented for now"
);
} }
#[test] #[test]
fn content() { fn content() {
let vfs = vfs(); let vfs = vfs();
let content = vfs.content(VPath::new("test.txt")); let content = query::<Content>(&vfs, VPath::new("test.txt")).map(Content::bytes);
assert_eq!(content.as_deref(), Some(b"hewwo :3\n".as_slice())); assert_eq!(content.as_deref(), Some(b"hewwo :3\n".as_slice()));
} }