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:
parent
5b6d637f44
commit
600651ec16
29 changed files with 418 additions and 611 deletions
|
@ -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() {
|
||||||
|
|
|
@ -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 =
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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="{}""#,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
mod cd;
|
mod cd;
|
||||||
mod empty;
|
|
||||||
mod file;
|
mod file;
|
||||||
mod mount_points;
|
mod mount_points;
|
||||||
mod physical;
|
mod physical;
|
||||||
|
|
|
@ -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)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
|
||||||
}
|
|
|
@ -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()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()));
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue